/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */

//! Style resolution for a given element or pseudo-element.

use crate::applicable_declarations::ApplicableDeclarationList;
use crate::computed_value_flags::ComputedValueFlags;
use crate::context::{CascadeInputs, ElementCascadeInputs, StyleContext};
use crate::data::{EagerPseudoStyles, ElementStyles};
use crate::dom::TElement;
use crate::matching::MatchMethods;
use crate::properties::longhands::display::computed_value::T as Display;
use crate::properties::{ComputedValues, FirstLineReparenting};
use crate::rule_tree::StrongRuleNode;
use crate::selector_parser::{PseudoElement, SelectorImpl};
use crate::stylist::RuleInclusion;
use log::Level::Trace;
use selectors::matching::{
    IncludeStartingStyle, MatchingContext, MatchingForInvalidation, MatchingMode,
    NeedsSelectorFlags, VisitedHandlingMode,
};
use selectors::parser::PseudoElement as PseudoElementTrait;
use servo_arc::Arc;

/// Whether pseudo-elements should be resolved or not.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum PseudoElementResolution {
    /// Only resolve pseudo-styles if possibly applicable.
    IfApplicable,
    /// Force pseudo-element resolution.
    Force,
}

/// A struct that takes care of resolving the style of a given element.
pub struct StyleResolverForElement<'a, 'ctx, 'le, E>
where
    'ctx: 'a,
    'le: 'ctx,
    E: TElement + MatchMethods + 'le,
{
    element: E,
    context: &'a mut StyleContext<'ctx, E>,
    rule_inclusion: RuleInclusion,
    pseudo_resolution: PseudoElementResolution,
    _marker: ::std::marker::PhantomData<&'le E>,
}

struct MatchingResults {
    rule_node: StrongRuleNode,
    flags: ComputedValueFlags,
    has_starting_style: bool,
}

/// A style returned from the resolver machinery.
pub struct ResolvedStyle(pub Arc<ComputedValues>);

impl ResolvedStyle {
    /// Convenience accessor for the style.
    #[inline]
    pub fn style(&self) -> &ComputedValues {
        &*self.0
    }
}

/// The primary style of an element or an element-backed pseudo-element.
pub struct PrimaryStyle {
    /// The style itself.
    pub style: ResolvedStyle,
    /// Whether the style was reused from another element via the rule node (see
    /// `StyleSharingCache::lookup_by_rules`).
    pub reused_via_rule_node: bool,
    /// The element may have matched rules inside @starting-style.
    /// Basically, we don't apply @starting-style rules to |style|. This is a sugar to let us know
    /// if we should resolve the element again for starting style, which is the after-change style
    /// with @starting-style rules applied in addition.
    pub may_have_starting_style: bool,
}

/// A set of style returned from the resolver machinery.
pub struct ResolvedElementStyles {
    /// Primary style.
    pub primary: PrimaryStyle,
    /// Pseudo styles.
    pub pseudos: EagerPseudoStyles,
}

impl ResolvedElementStyles {
    /// Convenience accessor for the primary style.
    pub fn primary_style(&self) -> &Arc<ComputedValues> {
        &self.primary.style.0
    }

    /// Convenience mutable accessor for the style.
    pub fn primary_style_mut(&mut self) -> &mut Arc<ComputedValues> {
        &mut self.primary.style.0
    }

    /// Returns true if this element may have starting style rules.
    #[inline]
    pub fn may_have_starting_style(&self) -> bool {
        self.primary.may_have_starting_style
    }
}

impl PrimaryStyle {
    /// Convenience accessor for the style.
    pub fn style(&self) -> &ComputedValues {
        &*self.style.0
    }
}

impl From<ResolvedElementStyles> for ElementStyles {
    fn from(r: ResolvedElementStyles) -> ElementStyles {
        ElementStyles {
            primary: Some(r.primary.style.0),
            pseudos: r.pseudos,
        }
    }
}

pub(crate) fn with_default_parent_styles<E, F, R>(element: E, f: F) -> R
where
    E: TElement,
    F: FnOnce(Option<&ComputedValues>, Option<&ComputedValues>) -> R,
{
    let parent_el = element.inheritance_parent();
    let parent_data = parent_el.as_ref().and_then(|e| e.borrow_data());
    let parent_style = parent_data.as_ref().map(|d| d.styles.primary());

    let mut layout_parent_el = parent_el.clone();
    let layout_parent_data;
    let mut layout_parent_style = parent_style;
    if parent_style.map_or(false, |s| s.is_display_contents()) {
        layout_parent_el = Some(layout_parent_el.unwrap().layout_parent());
        layout_parent_data = layout_parent_el.as_ref().unwrap().borrow_data().unwrap();
        layout_parent_style = Some(layout_parent_data.styles.primary());
    }

    f(
        parent_style.map(|x| &**x),
        layout_parent_style.map(|s| &**s),
    )
}

fn layout_parent_style_for_pseudo<'a>(
    primary_style: &'a PrimaryStyle,
    layout_parent_style: Option<&'a ComputedValues>,
) -> Option<&'a ComputedValues> {
    if primary_style.style().is_display_contents() {
        layout_parent_style
    } else {
        Some(primary_style.style())
    }
}

fn eager_pseudo_is_definitely_not_generated(
    pseudo: &PseudoElement,
    style: &ComputedValues,
) -> bool {
    if !pseudo.is_before_or_after() {
        return false;
    }

    if !style
        .flags
        .intersects(ComputedValueFlags::DISPLAY_DEPENDS_ON_INHERITED_STYLE)
        && style.get_box().clone_display() == Display::None
    {
        return true;
    }

    if !style
        .flags
        .intersects(ComputedValueFlags::CONTENT_DEPENDS_ON_INHERITED_STYLE)
        && style.ineffective_content_property()
    {
        return true;
    }

    false
}

impl<'a, 'ctx, 'le, E> StyleResolverForElement<'a, 'ctx, 'le, E>
where
    'ctx: 'a,
    'le: 'ctx,
    E: TElement + MatchMethods + 'le,
{
    /// Trivially construct a new StyleResolverForElement.
    pub fn new(
        element: E,
        context: &'a mut StyleContext<'ctx, E>,
        rule_inclusion: RuleInclusion,
        pseudo_resolution: PseudoElementResolution,
    ) -> Self {
        Self {
            element,
            context,
            rule_inclusion,
            pseudo_resolution,
            _marker: ::std::marker::PhantomData,
        }
    }

    /// Resolve just the style of a given element.
    pub fn resolve_primary_style(
        &mut self,
        parent_style: Option<&ComputedValues>,
        layout_parent_style: Option<&ComputedValues>,
        include_starting_style: IncludeStartingStyle,
    ) -> PrimaryStyle {
        let primary_results = self.match_primary(
            VisitedHandlingMode::AllLinksUnvisited,
            include_starting_style,
        );

        let inside_link = parent_style.map_or(false, |s| s.visited_style().is_some());

        let visited_rules = if self.context.shared.visited_styles_enabled
            && (inside_link || self.element.is_link())
        {
            let visited_matching_results = self.match_primary(
                VisitedHandlingMode::RelevantLinkVisited,
                IncludeStartingStyle::No,
            );
            Some(visited_matching_results.rule_node)
        } else {
            None
        };

        self.cascade_primary_style(
            CascadeInputs {
                rules: Some(primary_results.rule_node),
                visited_rules,
                flags: primary_results.flags,
            },
            parent_style,
            layout_parent_style,
            include_starting_style,
            primary_results.has_starting_style,
        )
    }

    fn cascade_primary_style(
        &mut self,
        inputs: CascadeInputs,
        parent_style: Option<&ComputedValues>,
        layout_parent_style: Option<&ComputedValues>,
        include_starting_style: IncludeStartingStyle,
        may_have_starting_style: bool,
    ) -> PrimaryStyle {
        // Before doing the cascade, check the sharing cache and see if we can
        // reuse the style via rule node identity.
        let may_reuse = self.element.matches_user_and_content_rules()
            && parent_style.is_some()
            && inputs.rules.is_some()
            && include_starting_style == IncludeStartingStyle::No;

        if may_reuse {
            let cached = self.context.thread_local.sharing_cache.lookup_by_rules(
                self.context.shared,
                parent_style.unwrap(),
                inputs.rules.as_ref().unwrap(),
                inputs.visited_rules.as_ref(),
                self.element,
            );
            if let Some(mut primary_style) = cached {
                self.context.thread_local.statistics.styles_reused += 1;
                primary_style.reused_via_rule_node |= true;
                return primary_style;
            }
        }

        // No style to reuse. Cascade the style, starting with visited style
        // if necessary.
        PrimaryStyle {
            style: self.cascade_style_and_visited(
                inputs,
                parent_style,
                layout_parent_style,
                /* pseudo = */ None,
            ),
            reused_via_rule_node: false,
            may_have_starting_style,
        }
    }

    /// Resolve the style of a given element, and all its eager pseudo-elements.
    pub fn resolve_style(
        &mut self,
        parent_style: Option<&ComputedValues>,
        layout_parent_style: Option<&ComputedValues>,
    ) -> ResolvedElementStyles {
        let primary_style =
            self.resolve_primary_style(parent_style, layout_parent_style, IncludeStartingStyle::No);

        let mut pseudo_styles = EagerPseudoStyles::default();

        if !self
            .element
            .implemented_pseudo_element()
            .is_some_and(|p| !PseudoElementTrait::is_element_backed(&p))
        {
            let layout_parent_style_for_pseudo =
                layout_parent_style_for_pseudo(&primary_style, layout_parent_style);
            SelectorImpl::each_eagerly_cascaded_pseudo_element(|pseudo| {
                let pseudo_style = self.resolve_pseudo_style(
                    &pseudo,
                    &primary_style,
                    layout_parent_style_for_pseudo,
                );

                if let Some(style) = pseudo_style {
                    if !matches!(self.pseudo_resolution, PseudoElementResolution::Force)
                        && eager_pseudo_is_definitely_not_generated(&pseudo, &style.0)
                    {
                        return;
                    }
                    pseudo_styles.set(&pseudo, style.0);
                }
            })
        }

        ResolvedElementStyles {
            primary: primary_style,
            pseudos: pseudo_styles,
        }
    }

    /// Resolve an element's styles with the default inheritance parent/layout
    /// parents.
    pub fn resolve_style_with_default_parents(&mut self) -> ResolvedElementStyles {
        with_default_parent_styles(self.element, |parent_style, layout_parent_style| {
            self.resolve_style(parent_style, layout_parent_style)
        })
    }

    /// Cascade a set of rules, using the default parent for inheritance.
    pub fn cascade_style_and_visited_with_default_parents(
        &mut self,
        inputs: CascadeInputs,
    ) -> ResolvedStyle {
        with_default_parent_styles(self.element, |parent_style, layout_parent_style| {
            self.cascade_style_and_visited(
                inputs,
                parent_style,
                layout_parent_style,
                /* pseudo = */ None,
            )
        })
    }

    /// Cascade a set of rules for pseudo element, using the default parent for inheritance.
    pub fn cascade_style_and_visited_for_pseudo_with_default_parents(
        &mut self,
        inputs: CascadeInputs,
        pseudo: &PseudoElement,
        primary_style: &PrimaryStyle,
    ) -> ResolvedStyle {
        with_default_parent_styles(self.element, |_, layout_parent_style| {
            let layout_parent_style_for_pseudo =
                layout_parent_style_for_pseudo(primary_style, layout_parent_style);

            self.cascade_style_and_visited(
                inputs,
                Some(primary_style.style()),
                layout_parent_style_for_pseudo,
                Some(pseudo),
            )
        })
    }

    fn cascade_style_and_visited(
        &mut self,
        inputs: CascadeInputs,
        parent_style: Option<&ComputedValues>,
        layout_parent_style: Option<&ComputedValues>,
        pseudo: Option<&PseudoElement>,
    ) -> ResolvedStyle {
        debug_assert!(pseudo.map_or(true, |p| p.is_eager()));

        let mut conditions = Default::default();
        let values = self.context.shared.stylist.cascade_style_and_visited(
            Some(self.element),
            pseudo,
            inputs,
            &self.context.shared.guards,
            parent_style,
            layout_parent_style,
            FirstLineReparenting::No,
            /* try_tactic = */ &Default::default(),
            Some(&self.context.thread_local.rule_cache),
            &mut conditions,
        );

        self.context.thread_local.rule_cache.insert_if_possible(
            &self.context.shared.guards,
            &values,
            pseudo,
            &conditions,
        );

        ResolvedStyle(values)
    }

    /// Cascade the element and pseudo-element styles with the default parents.
    pub fn cascade_styles_with_default_parents(
        &mut self,
        inputs: ElementCascadeInputs,
        may_have_starting_style: bool,
    ) -> ResolvedElementStyles {
        with_default_parent_styles(self.element, move |parent_style, layout_parent_style| {
            let primary_style = self.cascade_primary_style(
                inputs.primary,
                parent_style,
                layout_parent_style,
                IncludeStartingStyle::No,
                may_have_starting_style,
            );

            let mut pseudo_styles = EagerPseudoStyles::default();
            if let Some(mut pseudo_array) = inputs.pseudos.into_array() {
                let layout_parent_style_for_pseudo = if primary_style.style().is_display_contents()
                {
                    layout_parent_style
                } else {
                    Some(primary_style.style())
                };

                for (i, inputs) in pseudo_array.iter_mut().enumerate() {
                    if let Some(inputs) = inputs.take() {
                        let pseudo = PseudoElement::from_eager_index(i);

                        let style = self.cascade_style_and_visited(
                            inputs,
                            Some(primary_style.style()),
                            layout_parent_style_for_pseudo,
                            Some(&pseudo),
                        );

                        if !matches!(self.pseudo_resolution, PseudoElementResolution::Force)
                            && eager_pseudo_is_definitely_not_generated(&pseudo, &style.0)
                        {
                            continue;
                        }

                        pseudo_styles.set(&pseudo, style.0);
                    }
                }
            }

            ResolvedElementStyles {
                primary: primary_style,
                pseudos: pseudo_styles,
            }
        })
    }

    fn resolve_pseudo_style(
        &mut self,
        pseudo: &PseudoElement,
        originating_element_style: &PrimaryStyle,
        layout_parent_style: Option<&ComputedValues>,
    ) -> Option<ResolvedStyle> {
        let MatchingResults {
            rule_node,
            mut flags,
            has_starting_style: _,
        } = self.match_pseudo(
            &originating_element_style.style.0,
            pseudo,
            VisitedHandlingMode::AllLinksUnvisited,
        )?;

        let mut visited_rules = None;
        if originating_element_style.style().visited_style().is_some() {
            visited_rules = self
                .match_pseudo(
                    &originating_element_style.style.0,
                    pseudo,
                    VisitedHandlingMode::RelevantLinkVisited,
                )
                .map(|results| {
                    flags |= results.flags;
                    results.rule_node
                });
        }

        Some(self.cascade_style_and_visited(
            CascadeInputs {
                rules: Some(rule_node),
                visited_rules,
                flags,
            },
            Some(originating_element_style.style()),
            layout_parent_style,
            Some(pseudo),
        ))
    }

    fn match_primary(
        &mut self,
        visited_handling: VisitedHandlingMode,
        include_starting_style: IncludeStartingStyle,
    ) -> MatchingResults {
        debug!(
            "Match primary for {:?}, visited: {:?}",
            self.element, visited_handling
        );
        let mut applicable_declarations = ApplicableDeclarationList::new();

        let bloom_filter = self.context.thread_local.bloom_filter.filter();
        let selector_caches = &mut self.context.thread_local.selector_caches;
        let mut matching_context = MatchingContext::new_for_visited(
            MatchingMode::Normal,
            Some(bloom_filter),
            selector_caches,
            visited_handling,
            include_starting_style,
            self.context.shared.quirks_mode(),
            NeedsSelectorFlags::Yes,
            MatchingForInvalidation::No,
        );

        let stylist = &self.context.shared.stylist;
        // Compute the primary rule node.
        stylist.push_applicable_declarations(
            self.element,
            None,
            self.element.style_attribute(),
            self.element.smil_override(),
            self.element.animation_declarations(self.context.shared),
            self.rule_inclusion,
            &mut applicable_declarations,
            &mut matching_context,
        );

        // FIXME(emilio): This is a hack for animations, and should go away.
        self.element.unset_dirty_style_attribute();

        let rule_node = stylist
            .rule_tree()
            .compute_rule_node(&mut applicable_declarations, &self.context.shared.guards);

        if log_enabled!(Trace) {
            trace!("Matched rules for {:?}:", self.element);
            for rn in rule_node.self_and_ancestors() {
                let source = rn.style_source();
                if source.is_some() {
                    trace!(" > {:?}", source);
                }
            }
        }

        MatchingResults {
            rule_node,
            flags: matching_context.extra_data.cascade_input_flags,
            has_starting_style: matching_context.has_starting_style,
        }
    }

    fn match_pseudo(
        &mut self,
        originating_element_style: &ComputedValues,
        pseudo_element: &PseudoElement,
        visited_handling: VisitedHandlingMode,
    ) -> Option<MatchingResults> {
        debug!(
            "Match pseudo {:?} for {:?}, visited: {:?}",
            self.element, pseudo_element, visited_handling
        );
        debug_assert!(pseudo_element.is_eager());

        let mut applicable_declarations = ApplicableDeclarationList::new();

        let stylist = &self.context.shared.stylist;

        if !self
            .element
            .may_generate_pseudo(pseudo_element, originating_element_style)
        {
            return None;
        }

        let bloom_filter = self.context.thread_local.bloom_filter.filter();
        let selector_caches = &mut self.context.thread_local.selector_caches;

        let mut matching_context = MatchingContext::<'_, E::Impl>::new_for_visited(
            MatchingMode::ForStatelessPseudoElement,
            Some(bloom_filter),
            selector_caches,
            visited_handling,
            IncludeStartingStyle::No,
            self.context.shared.quirks_mode(),
            NeedsSelectorFlags::Yes,
            MatchingForInvalidation::No,
        );
        matching_context.extra_data.originating_element_style = Some(originating_element_style);

        // NB: We handle animation rules for ::before and ::after when
        // traversing them.
        stylist.push_applicable_declarations(
            self.element,
            Some(pseudo_element),
            None,
            None,
            /* animation_declarations = */ Default::default(),
            self.rule_inclusion,
            &mut applicable_declarations,
            &mut matching_context,
        );

        if applicable_declarations.is_empty() {
            return None;
        }

        let rule_node = stylist
            .rule_tree()
            .compute_rule_node(&mut applicable_declarations, &self.context.shared.guards);

        Some(MatchingResults {
            rule_node,
            flags: matching_context.extra_data.cascade_input_flags,
            has_starting_style: false, // We don't care.
        })
    }

    /// Resolve the starting style.
    pub fn resolve_starting_style(&mut self) -> PrimaryStyle {
        // Compute after-change style for the parent and the layout parent.
        // Per spec, starting style inherits from the parent’s after-change style just like
        // after-change style does.
        let parent_el = self.element.inheritance_parent();
        let parent_data = parent_el.as_ref().and_then(|e| e.borrow_data());
        let parent_style = parent_data.as_ref().map(|d| d.styles.primary());
        let parent_after_change_style = parent_style.and_then(|s| self.after_change_style(s));
        let parent_values = parent_after_change_style
            .as_ref()
            .or(parent_style)
            .map(|x| &**x);

        let mut layout_parent_el = parent_el.clone();
        let layout_parent_data;
        let layout_parent_after_change_style;
        let layout_parent_values = if parent_style.map_or(false, |s| s.is_display_contents()) {
            layout_parent_el = Some(layout_parent_el.unwrap().layout_parent());
            layout_parent_data = layout_parent_el.as_ref().unwrap().borrow_data().unwrap();
            let layout_parent_style = Some(layout_parent_data.styles.primary());
            layout_parent_after_change_style =
                layout_parent_style.and_then(|s| self.after_change_style(s));
            layout_parent_after_change_style
                .as_ref()
                .or(layout_parent_style)
                .map(|x| &**x)
        } else {
            parent_values
        };

        self.resolve_primary_style(
            parent_values,
            layout_parent_values,
            IncludeStartingStyle::Yes,
        )
    }

    /// If there is no transition rule in the ComputedValues, it returns None.
    pub fn after_change_style(
        &mut self,
        primary_style: &Arc<ComputedValues>,
    ) -> Option<Arc<ComputedValues>> {
        let rule_node = primary_style.rules();
        let without_transition_rules = self
            .context
            .shared
            .stylist
            .rule_tree()
            .remove_transition_rule_if_applicable(rule_node);
        if without_transition_rules == *rule_node {
            // We don't have transition rule in this case, so return None to let
            // the caller use the original ComputedValues.
            return None;
        }

        // FIXME(bug 868975): We probably need to transition visited style as
        // well.
        let inputs = CascadeInputs {
            rules: Some(without_transition_rules),
            visited_rules: primary_style.visited_rules().cloned(),
            flags: primary_style.flags.for_cascade_inputs(),
        };

        let style = self.cascade_style_and_visited_with_default_parents(inputs);
        Some(style.0)
    }
}
