use crate::*;

/// SAFETY: `InjectedClassesCell` is only used in single-threaded WASM contexts.
unsafe impl Sync for InjectedClassesCell {}

/// Implementation of attribute value factory methods for reactive and merged values.
impl AttributeValue {
    /// Creates a reactive attribute `Self` for conditional attribute values.
    ///
    /// This function replaces the inline `Signal::create(...)` + `subscribe_attr(...)`
    /// boilerplate that was previously generated by the `html!` macro for every
    /// attribute value containing an `if` condition.
    ///
    /// # Arguments
    ///
    /// - `Fn() -> String + 'static` - A closure that computes the current attribute value.
    ///   Called on initial render and whenever any signal changes.
    ///
    /// # Returns
    ///
    /// - `Self` - A `Self::Signal` backed by a `Signal<String>`
    ///   that reactively re-evaluates the attribute value on signal updates.
    pub fn create_reactive_signal<F>(compute: F) -> Self
    where
        F: Fn() -> String + 'static,
    {
        let attr_signal: Signal<String> =
            Signal::create(IntoReactiveString::into_reactive_string(compute()));
        subscribe_attr(attr_signal, move || {
            IntoReactiveString::into_reactive_string(compute())
        });
        Self::Signal(attr_signal)
    }

    /// Merges multiple class attribute values into a single `Self`.
    ///
    /// Each input value is adapted into a `Self` via `IntoReactiveValue`.
    /// `Css` values are injected into the DOM and their names are collected.
    /// All non-empty class names are joined with spaces into a final `Text` attribute.
    /// If any value is signal-backed, the result becomes a reactive `Signal` attribute
    /// that re-evaluates when any constituent signal changes.
    ///
    /// # Arguments
    ///
    /// - `&[Self]` - The class attribute values to merge.
    ///
    /// # Returns
    ///
    /// - `Self` - A merged attribute value containing space-separated class names.
    pub fn merge_class(values: &[Self]) -> Self {
        let has_signal: bool = values
            .iter()
            .any(|value: &Self| matches!(value, Self::Signal(_)));
        if has_signal {
            let owned_values: Vec<Self> = values.to_vec();
            let compute: Box<dyn Fn() -> String> = Box::new(move || {
                owned_values
                    .iter()
                    .filter_map(|value: &Self| match value {
                        Self::Css(css) => {
                            css.inject_style();
                            Some(css.get_name().to_string())
                        }
                        Self::Text(text_value) => Some(text_value.clone()),
                        Self::Signal(signal) => Some(signal.get()),
                        _ => None,
                    })
                    .filter(|segment: &String| !segment.is_empty())
                    .collect::<Vec<String>>()
                    .join(&CHAR_SPACE.to_string())
            });
            let attr_signal: Signal<String> = Signal::create(compute());
            subscribe_attr(attr_signal, compute);
            Self::Signal(attr_signal)
        } else {
            let result: String = values
                .iter()
                .filter_map(|value: &Self| match value {
                    Self::Css(css) => {
                        css.inject_style();
                        Some(css.get_name().to_string())
                    }
                    Self::Text(text_value) => Some(text_value.clone()),
                    _ => None,
                })
                .filter(|segment: &String| !segment.is_empty())
                .collect::<Vec<String>>()
                .join(&CHAR_SPACE.to_string());
            Self::Text(result)
        }
    }

    /// Merges multiple style attribute values into a single `Self`.
    ///
    /// Each input value is expected to be a style string (`Text`) or a reactive
    /// `Signal<String>` producing a style string. All non-empty style strings are
    /// joined with spaces into a final combined style attribute.
    /// If any value is signal-backed, the result becomes a reactive `Signal` attribute.
    ///
    /// # Arguments
    ///
    /// - `&[Self]` - The style attribute values to merge.
    ///
    /// # Returns
    ///
    /// - `Self` - A merged attribute value containing the combined CSS style string.
    pub fn merge_style(values: &[Self]) -> Self {
        let has_signal: bool = values
            .iter()
            .any(|value: &Self| matches!(value, Self::Signal(_)));
        if has_signal {
            let owned_values: Vec<Self> = values.to_vec();
            let compute: Box<dyn Fn() -> String> = Box::new(move || {
                owned_values
                    .iter()
                    .filter_map(|value: &Self| match value {
                        Self::Text(text_value) => Some(text_value.clone()),
                        Self::Signal(signal) => Some(signal.get()),
                        _ => None,
                    })
                    .filter(|segment: &String| !segment.is_empty())
                    .collect::<Vec<String>>()
                    .join(&CHAR_SPACE.to_string())
            });
            let attr_signal: Signal<String> = Signal::create(compute());
            subscribe_attr(attr_signal, compute);
            Self::Signal(attr_signal)
        } else {
            let result: String = values
                .iter()
                .filter_map(|value: &Self| match value {
                    Self::Text(text_value) => Some(text_value.clone()),
                    _ => None,
                })
                .filter(|segment: &String| !segment.is_empty())
                .collect::<Vec<String>>()
                .join(&CHAR_SPACE.to_string());
            Self::Text(result)
        }
    }
}

/// Visual equality comparison for attribute values.
///
/// Compares values by their visual output rather than identity. `Signal`
/// values are compared by their current resolved string; when both signals
/// share the same inner pointer, they are always considered **unequal**
/// because the signal may have mutated between VDOM snapshots and `.get()`
/// would return the same current value for both, masking the change.
/// `Event` values are always considered equal (re-binding is handled by the
/// handler registry), and `Css` values are compared by class name.
impl PartialEq for AttributeValue {
    /// Compares two attribute values for visual equality.
    ///
    /// # Arguments
    ///
    /// - `&Self` - The first attribute value.
    /// - `&Self` - The second attribute value.
    ///
    /// # Returns
    ///
    /// - `bool` - `true` if the values are visually equal.
    fn eq(&self, other: &Self) -> bool {
        match (self, other) {
            (Self::Text(old_value), Self::Text(new_value)) => old_value == new_value,
            (Self::Signal(old_signal), Self::Signal(new_signal)) => {
                if old_signal.get_inner() == new_signal.get_inner() {
                    return false;
                }
                old_signal.get() == new_signal.get()
            }
            (Self::Signal(old_signal), Self::Text(new_value)) => old_signal.get() == *new_value,
            (Self::Text(old_value), Self::Signal(new_signal)) => *old_value == new_signal.get(),
            (Self::Event(_), Self::Event(_)) => true,
            (Self::Css(old_class), Self::Css(new_class)) => {
                old_class.get_name() == new_class.get_name()
            }
            (Self::Dynamic(old_dynamic), Self::Dynamic(new_dynamic)) => old_dynamic == new_dynamic,
            _ => false,
        }
    }
}

/// Visual equality comparison for attribute entries.
///
/// Two attribute entries are equal when their names match and their values
/// are visually equal as defined by `AttributeValue::eq`.
impl PartialEq for AttributeEntry {
    /// Compares two attribute entries for visual equality.
    ///
    /// # Arguments
    ///
    /// - `&Self` - The first attribute entry.
    /// - `&Self` - The second attribute entry.
    ///
    /// # Returns
    ///
    /// - `bool` - `true` if both names and values match.
    fn eq(&self, other: &Self) -> bool {
        self.get_name() == other.get_name() && self.get_value() == other.get_value()
    }
}

/// Visual equality comparison for CSS classes.
///
/// Two CSS classes are considered equal when their class names match,
/// since the name uniquely identifies the visual style rule.
impl PartialEq for Css {
    /// Compares two CSS classes by name.
    ///
    /// # Arguments
    ///
    /// - `&Self` - The first CSS class.
    /// - `&Self` - The second CSS class.
    ///
    /// # Returns
    ///
    /// - `bool` - `true` if the class names match.
    fn eq(&self, other: &Self) -> bool {
        self.get_name() == other.get_name()
    }
}

/// Implementation of Css construction and style injection.
impl Css {
    /// Parses pseudo-class/pseudo-element rules from a compact serialization string.
    ///
    /// The serialization format is: `:selector { key: value; key: value; }:another { ... }`
    /// This is used by the `class!` macro for fully static class definitions
    /// where pseudo rules can be computed at compile time.
    ///
    /// # Arguments
    ///
    /// - `I: AsRef<str>` - The serialized pseudo rules string.
    ///
    /// # Returns
    ///
    /// - `Vec<PseudoRule>` - The parsed pseudo rules.
    pub fn parse_pseudo_rules<I>(input: I) -> Vec<PseudoRule>
    where
        I: AsRef<str>,
    {
        let mut remaining: &str = input.as_ref();
        let mut rules: Vec<PseudoRule> = Vec::new();
        while !remaining.is_empty() {
            let selector_end: Option<usize> = remaining.find(CSS_RULE_OPEN);
            let Some(selector_end_index) = selector_end else {
                break;
            };
            let selector: &str = &remaining[..selector_end_index];
            let after_selector: &str = remaining[selector_end_index..]
                .strip_prefix(CSS_RULE_OPEN)
                .unwrap_or_default();
            let style_end: Option<usize> = after_selector.find(CHAR_CSS_RULE_CLOSE);
            let Some(style_end_index) = style_end else {
                break;
            };
            let style: &str = &after_selector[..style_end_index];
            if !selector.is_empty() && !style.is_empty() {
                rules.push(PseudoRule::new(selector.to_string(), style.to_string()));
            }
            remaining = after_selector[style_end_index..]
                .strip_prefix(CHAR_CSS_RULE_CLOSE)
                .unwrap_or_default();
        }
        rules
    }

    /// Parses media query rules from a compact serialization string.
    ///
    /// The serialization format is:
    /// `@media query { key: value; ::selector { key: value; } }@media query2 { ... }`
    /// This is used by the `class!` macro for fully static class definitions
    /// where media rules can be computed at compile time.
    /// Supports nested pseudo-element blocks inside media query blocks.
    ///
    /// # Arguments
    ///
    /// - `S: AsRef<str>` - The serialized media rules string.
    ///
    /// # Returns
    ///
    /// - `Vec<MediaRule>` - The parsed media rules.
    pub fn parse_media_rules<S>(input: S) -> Vec<MediaRule>
    where
        S: AsRef<str>,
    {
        let input: &str = input.as_ref();
        let mut rules: Vec<MediaRule> = Vec::new();
        let mut remaining: &str = input;
        while !remaining.is_empty() {
            if !remaining.starts_with(CSS_MEDIA_PREFIX) {
                break;
            }
            let after_prefix: &str = remaining.strip_prefix(CSS_MEDIA_PREFIX).unwrap_or_default();
            let query_end: Option<usize> = after_prefix.find(CSS_RULE_OPEN);
            let Some(query_end_index) = query_end else {
                break;
            };
            let query: &str = &after_prefix[..query_end_index];
            let after_query: &str = after_prefix[query_end_index..]
                .strip_prefix(CSS_RULE_OPEN)
                .unwrap_or_default();
            let mut depth: usize = 1;
            let mut close_pos: usize = 0;
            for (index, char_value) in after_query.char_indices() {
                if char_value == '{' {
                    depth += 1;
                } else if char_value == '}' {
                    depth -= 1;
                    if depth == 0 {
                        close_pos = index;
                        break;
                    }
                }
            }
            if close_pos == 0 {
                break;
            }
            let body: &str = &after_query[..close_pos];
            let (style, pseudo_rules): (String, Vec<PseudoRule>) = Self::parse_media_body(body);
            if !query.is_empty() && (!style.is_empty() || !pseudo_rules.is_empty()) {
                rules.push(MediaRule::new(query.to_string(), style, pseudo_rules));
            }
            remaining = after_query[close_pos..]
                .strip_prefix(CHAR_CSS_RULE_CLOSE)
                .unwrap_or_default();
        }
        rules
    }

    /// Parses the body of a media rule, separating top-level style declarations
    /// from nested pseudo-element blocks.
    ///
    /// # Arguments
    ///
    /// - `&str` - The media rule body content (between the outer braces).
    ///
    /// # Returns
    ///
    /// - `(String, Vec<PseudoRule>)` - A tuple of the style string and pseudo rules.
    fn parse_media_body(body: &str) -> (String, Vec<PseudoRule>) {
        let mut style_parts: String = String::new();
        let mut pseudo_rules: Vec<PseudoRule> = Vec::new();
        let mut remaining: &str = body;
        while !remaining.is_empty() {
            let brace_pos: Option<usize> = remaining.find('{');
            match brace_pos {
                Some(pos) => {
                    let before_brace: &str = remaining[..pos].trim();
                    if before_brace.starts_with("::") || before_brace.starts_with(':') {
                        let selector: &str = before_brace;
                        let after_brace: &str = &remaining[pos + 1..];
                        let mut depth: usize = 1;
                        let mut close_pos: usize = 0;
                        for (index, char_value) in after_brace.char_indices() {
                            if char_value == '{' {
                                depth += 1;
                            } else if char_value == '}' {
                                depth -= 1;
                                if depth == 0 {
                                    close_pos = index;
                                    break;
                                }
                            }
                        }
                        if close_pos > 0 {
                            let inner_style: &str = after_brace[..close_pos].trim();
                            if !selector.is_empty() && !inner_style.is_empty() {
                                pseudo_rules.push(PseudoRule::new(
                                    selector.to_string(),
                                    inner_style.to_string(),
                                ));
                            }
                            remaining = after_brace[close_pos + 1..].trim_start();
                            continue;
                        }
                        break;
                    } else {
                        style_parts.push_str(before_brace);
                        style_parts.push(' ');
                        let after_brace: &str = &remaining[pos + 1..];
                        let mut depth: usize = 1;
                        let mut close_pos: usize = 0;
                        for (index, char_value) in after_brace.char_indices() {
                            if char_value == '{' {
                                depth += 1;
                            } else if char_value == '}' {
                                depth -= 1;
                                if depth == 0 {
                                    close_pos = index;
                                    break;
                                }
                            }
                        }
                        if close_pos > 0 {
                            style_parts.push_str(after_brace[..close_pos].trim());
                            style_parts.push(' ');
                            remaining = after_brace[close_pos + 1..].trim_start();
                            continue;
                        }
                        break;
                    }
                }
                None => {
                    style_parts.push_str(remaining.trim());
                    break;
                }
            }
        }
        (style_parts.trim().to_string(), pseudo_rules)
    }

    /// Injects this class's styles into the DOM if not already present.
    ///
    /// Uses a global `HashSet` to track injected class names, avoiding the
    /// expensive `existing_css.contains(css)` full-text search on every call.
    /// Builds the class rule, pseudo-class rules, and media rules as CSS text,
    /// then appends them directly to the `<style>` element via
    /// `append_child` with a new text node — no read-modify-write of the
    /// entire stylesheet content.
    ///
    /// # Panics
    ///
    /// Panics if `window()` or `document()` is unavailable on the current platform.
    pub fn inject_style(&self) {
        if !Self::mark_injected(self.get_name().clone()) {
            return;
        }
        let mut css_text: String = format!(
            "{CHAR_CSS_CLASS_PREFIX}{}{CSS_RULE_OPEN_FORMAT}{}{CSS_RULE_CLOSE_FORMAT}",
            self.get_name(),
            self.get_style()
        );
        for pseudo_rule in self.get_pseudo_rules() {
            if !pseudo_rule.get_style().is_empty() {
                css_text = format!(
                    "{css_text}{CHAR_CSS_RULE_SEPARATOR}{CHAR_CSS_CLASS_PREFIX}{}{}{CSS_RULE_OPEN_FORMAT}{}{CSS_RULE_CLOSE_FORMAT}",
                    self.get_name(),
                    pseudo_rule.get_selector(),
                    pseudo_rule.get_style()
                );
            }
        }
        for media_rule in self.get_media_rules() {
            if !media_rule.get_query().is_empty() {
                let mut media_body: String = format!(
                    "{CHAR_CSS_CLASS_PREFIX}{}{CSS_RULE_OPEN_FORMAT}{}{CSS_RULE_CLOSE_FORMAT}",
                    self.get_name(),
                    media_rule.get_style()
                );
                for pseudo_rule in media_rule.get_pseudo_rules() {
                    if !pseudo_rule.get_style().is_empty() {
                        media_body = format!(
                            "{media_body} {CHAR_CSS_CLASS_PREFIX}{}{}{CSS_RULE_OPEN_FORMAT}{}{CSS_RULE_CLOSE_FORMAT}",
                            self.get_name(),
                            pseudo_rule.get_selector(),
                            pseudo_rule.get_style()
                        );
                    }
                }
                css_text = format!(
                    "{css_text}{CHAR_CSS_RULE_SEPARATOR}{CSS_MEDIA_PREFIX}{}{CSS_RULE_OPEN_FORMAT}{}{CSS_RULE_CLOSE_FORMAT}",
                    media_rule.get_query(),
                    media_body
                );
            }
        }
        Self::append_css(&css_text);
    }

    /// Marks a class name as injected in the global `HashSet`.
    ///
    /// Returns `false` if the class was already injected (no-op), `true`
    /// if this is the first injection.
    ///
    /// # Arguments
    ///
    /// - `String` - The class name to mark as injected.
    ///
    /// # Returns
    ///
    /// - `bool` - `true` if newly injected, `false` if already present.
    fn mark_injected(class_name: String) -> bool {
        get_injected_classes_mut().insert(class_name)
    }

    /// Appends CSS text directly to the shared `<style>` element.
    ///
    /// Creates a new text node and appends it as a child of the `<style>`
    /// element, avoiding the read-modify-write pattern of reading the entire
    /// `innerText`, concatenating, and setting it back.
    ///
    /// # Arguments
    ///
    /// - `&str` - The CSS text to append.
    ///
    fn append_css(css_text: &str) {
        let style_id: &str = EUV_CSS_INJECTED_ID;
        let window_value: Window = match window() {
            Some(window_instance) => window_instance,
            None => return,
        };
        let document: Document = match window_value.document() {
            Some(document_instance) => document_instance,
            None => return,
        };
        let style_element: HtmlStyleElement = match document.get_element_by_id(style_id) {
            Some(existing_element) => match existing_element.dyn_into::<HtmlStyleElement>() {
                Ok(element) => element,
                Err(_err) => return,
            },
            None => {
                let created: Element = match document.create_element(STYLE_TAG) {
                    Ok(element) => element,
                    Err(_err) => return,
                };
                let style_element_from_id: HtmlStyleElement =
                    match created.dyn_into::<HtmlStyleElement>() {
                        Ok(element) => element,
                        Err(_err) => return,
                    };
                style_element_from_id.set_id(style_id);
                if let Some(head) = document.head() {
                    let _ = head.append_child(&style_element_from_id);
                }
                style_element_from_id
            }
        };
        if !css_text.is_empty() {
            let text_node: Text = document.create_text_node(css_text);
            let _ = style_element.append_child(&text_node);
        }
    }

    /// Builds a CSS style string from an array of key-value pairs.
    ///
    /// This function is used by the `html!` macro to convert static `style:`
    /// attributes into a CSS string without allocating intermediate objects.
    ///
    /// # Arguments
    ///
    /// - `S: AsRef<str>` - An array of CSS property name-value pairs.
    ///
    /// # Returns
    ///
    /// - `String` - The CSS string (e.g., `"margin: 0 auto; max-width: 800px;"`).
    pub fn create_style_string<K, V>(props: &[(K, V)]) -> String
    where
        K: AsRef<str>,
        V: AsRef<str>,
    {
        props
            .iter()
            .map(|(key, value): &(K, V)| {
                format!(
                    "{}{CSS_PROP_SEPARATOR}{}{CHAR_CSS_DECL_TERMINATOR}",
                    key.as_ref(),
                    value.as_ref()
                )
            })
            .collect::<Vec<String>>()
            .join(&CHAR_SPACE.to_string())
    }

    /// Builds a CSS style string from owned key-value pairs.
    ///
    /// Used by the `html!` macro for reactive style attributes (with `if`
    /// conditions) where values are computed at runtime.
    ///
    /// # Arguments
    ///
    /// - `&[(String, String)]` - An array of owned CSS property name-value pairs.
    ///
    /// # Returns
    ///
    /// - `String` - The CSS string (e.g., `"margin: 0 auto; max-width: 800px;"`).
    pub fn create_style_string_owned(props: &[(String, String)]) -> String {
        props
            .iter()
            .map(|(key, value): &(String, String)| {
                format!("{key}{CSS_PROP_SEPARATOR}{value}{CHAR_CSS_DECL_TERMINATOR}")
            })
            .collect::<Vec<String>>()
            .join(&CHAR_SPACE.to_string())
    }

    /// Injects CSS text into the shared `<style>` element in the DOM.
    ///
    /// Delegates to [`Css::append_css`] for the actual DOM append.
    /// Unlike the previous implementation, this does not read the existing
    /// stylesheet content or perform a full-text `contains` search.
    ///
    /// # Arguments
    ///
    /// - `S: AsRef<str>` - The CSS text to inject (e.g., reset styles, keyframes, media queries).
    ///
    /// # Panics
    ///
    /// Panics if `window()` or `document()` is unavailable on the current platform.
    pub fn inject_css<S>(css_text: S)
    where
        S: AsRef<str>,
    {
        let css_text: &str = css_text.as_ref();
        Self::append_css(css_text);
    }
}

/// Displays the CSS class name.
///
/// This enables `format!("{}", css)` to produce the class name string,
/// which is required for reactive `if` conditions in `class:` attributes.
impl Display for Css {
    /// Formats the CSS class as its name string.
    ///
    /// # Arguments
    ///
    /// - `&mut Formatter` - The formatter.
    ///
    /// # Returns
    ///
    /// - `fmt::Result` - The formatting result.
    fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
        write!(formatter, "{}", self.get_name())
    }
}
