/* 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/. */

//! Gecko's definition of a pseudo-element.
//!
//! Note that a few autogenerated bits of this live in
//! `pseudo_element_definition.mako.rs`. If you touch that file, you probably
//! need to update the checked-in files for Servo.

use crate::gecko_bindings::structs::{self, PseudoStyleType};
use crate::properties::longhands::display::computed_value::T as Display;
use crate::properties::{ComputedValues, PropertyFlags};
use crate::selector_parser::{PseudoElementCascadeType, SelectorImpl};
use crate::str::{starts_with_ignore_ascii_case, string_as_ascii_lowercase};
use crate::string_cache::Atom;
use crate::values::serialize_atom_identifier;
use crate::values::AtomIdent;
use cssparser::{Parser, ToCss};
use selectors::parser::PseudoElement as PseudoElementTrait;
use static_prefs::pref;
use std::fmt;
use style_traits::ParseError;

include!(concat!(
    env!("OUT_DIR"),
    "/gecko/pseudo_element_definition.rs"
));

/// The target we are using for parsing pseudo-elements.
pub enum Target {
    /// When parsing a selector, we want to use the full syntax.
    Selector,
    /// When parsing the pseudo-element string (from CSSOM), we only accept CusomIdent for named
    /// view transition pseudo-elements.
    Cssom,
}

/// The type to hold the value of `<pt-name-and-class-selector>`.
///
/// `<pt-name-and-class-selector> = <pt-name-selector> <pt-class-selector>? | <pt-class-selector>`
/// `<pt-name-selector> = '*' | <custom-ident>`
/// `<pt-class-selector> = ['.' <custom-ident>]+`
///
/// This type should have at least one element.
/// If there is no <pt-name-selector>, the first element would be the universal symbol, i.e. '*'.
/// In other words, when we match it, ".abc" is the same as "*.abc".
/// Note that we also serialize ".abc" as "*.abc".
///
/// We use a single ThinVec<> to represent this structure to avoid allocating too much memory for a
/// single selectors::parser::Component (max: 24 bytes) and PseudoElement (max: 16 bytes).
///
/// https://drafts.csswg.org/css-view-transitions-2/#typedef-pt-name-and-class-selector
#[derive(Clone, Debug, Eq, Hash, MallocSizeOf, PartialEq, ToShmem)]
pub struct PtNameAndClassSelector(thin_vec::ThinVec<Atom>);

impl PtNameAndClassSelector {
    /// Constructs a new one from a name.
    pub fn from_name(name: Atom) -> Self {
        Self(thin_vec![name])
    }

    /// Returns the name component.
    pub fn name(&self) -> &Atom {
        debug_assert!(!self.0.is_empty());
        self.0.first().expect("Shouldn't be empty")
    }

    /// Returns the classes component.
    pub fn classes(&self) -> &[Atom] {
        debug_assert!(!self.0.is_empty());
        &self.0[1..]
    }

    /// Returns the vector we store.
    pub fn name_and_classes(&self) -> &thin_vec::ThinVec<Atom> {
        &self.0
    }

    /// Parse the pseudo-element tree name and/or class.
    /// |for_selector| is true if we are parsing the CSS selectors and so need to check the
    /// universal symbol, i.e. '*', and classes.
    // Note: We share the same type for both pseudo-element and pseudo-element selector. The
    // universal symbol (i.e. '*') and `<pt-class-selector>` are used only in the selector (for
    // matching).
    pub fn parse<'i, 't>(
        input: &mut Parser<'i, 't>,
        target: Target,
    ) -> Result<Self, ParseError<'i>> {
        use crate::values::CustomIdent;
        use cssparser::Token;
        use style_traits::StyleParseErrorKind;

        // <pt-name-selector> = '*' | <custom-ident>
        let parse_pt_name = |input: &mut Parser<'i, '_>| {
            // For pseudo-element string, we don't accept '*'.
            if matches!(target, Target::Selector)
                && input.try_parse(|i| i.expect_delim('*')).is_ok()
            {
                Ok(atom!("*"))
            } else {
                CustomIdent::parse(input, &[]).map(|c| c.0)
            }
        };
        let name = input.try_parse(parse_pt_name);

        // Skip <pt-class-selector> for pseudo-element string.
        if matches!(target, Target::Cssom) {
            return name.map(Self::from_name);
        }

        // <pt-class-selector> = ['.' <custom-ident>]+
        let parse_pt_class = |input: &mut Parser<'i, '_>| {
            // The white space is forbidden:
            // 1. Between <pt-name-selector> and <pt-class-selector>
            // 2. Between any of the components of <pt-class-selector>.
            let location = input.current_source_location();
            match input.next_including_whitespace()? {
                Token::Delim('.') => (),
                t => return Err(location.new_unexpected_token_error(t.clone())),
            }
            // Whitespace is not allowed between '.' and the class name.
            if let Ok(token) = input.try_parse(|i| i.expect_whitespace()) {
                return Err(input.new_unexpected_token_error(Token::WhiteSpace(token)));
            }
            CustomIdent::parse(input, &[]).map(|c| c.0)
        };
        // If there is no `<pt-name-selector>`, it's fine to have whitespaces before the first '.'.
        if name.is_err() {
            input.skip_whitespace();
        }
        let mut classes = thin_vec::ThinVec::new();
        while let Ok(class) = input.try_parse(parse_pt_class) {
            classes.push(class);
        }

        // If we don't have `<pt-name-selector>`, we must have `<pt-class-selector>`, per the
        // syntax: `<pt-name-selector> <pt-class-selector>? | <pt-class-selector>`.
        if name.is_err() && classes.is_empty() {
            return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError));
        }

        // Use the universal symbol as the first element to present the part of
        // `<pt-name-selector>` because they are equivalent (and the serialization is the same).
        let mut result = thin_vec![name.unwrap_or(atom!("*"))];
        result.append(&mut classes);

        Ok(Self(result))
    }
}

impl ToCss for PtNameAndClassSelector {
    fn to_css<W>(&self, dest: &mut W) -> fmt::Result
    where
        W: fmt::Write,
    {
        let name = self.name();
        if name == &atom!("*") {
            // serialize_atom_identifier() may serialize "*" as "\*", so we handle it separately.
            dest.write_char('*')?;
        } else {
            serialize_atom_identifier(name, dest)?;
        }

        for class in self.classes() {
            dest.write_char('.')?;
            serialize_atom_identifier(class, dest)?;
        }

        Ok(())
    }
}

impl PseudoElementTrait for PseudoElement {
    type Impl = SelectorImpl;

    // ::slotted() should support all tree-abiding pseudo-elements, see
    // https://drafts.csswg.org/css-scoping/#slotted-pseudo
    // https://drafts.csswg.org/css-pseudo-4/#treelike
    #[inline]
    fn valid_after_slotted(&self) -> bool {
        matches!(
            *self,
            Self::Before
                | Self::After
                | Self::Marker
                | Self::Placeholder
                | Self::FileSelectorButton
                | Self::DetailsContent
        )
    }

    // ::before/::after should support ::marker, but no others.
    // https://drafts.csswg.org/css-pseudo-4/#marker-pseudo
    #[inline]
    fn valid_after_before_or_after(&self) -> bool {
        matches!(*self, Self::Marker)
    }

    #[inline]
    fn accepts_state_pseudo_classes(&self) -> bool {
        // Note: if the pseudo element is a descendants of a pseudo element, `only-child` should be
        // allowed after it.
        self.supports_user_action_state() || self.is_in_pseudo_element_tree()
    }

    #[inline]
    fn specificity_count(&self) -> u32 {
        self.specificity_count()
    }

    #[inline]
    fn is_in_pseudo_element_tree(&self) -> bool {
        // All the named view transition pseudo-elements are the descendants of a pseudo-element
        // root.
        self.is_named_view_transition()
    }

    /// Whether this pseudo-element is "element-backed", which means that it inherits from its regular
    /// flat tree parent, which might not be the originating element.
    #[inline]
    fn is_element_backed(&self) -> bool {
        // Note: We don't include ::view-transition here because it inherits from the originating
        // element, instead of the snapshot containing block.
        self.is_named_view_transition() || *self == PseudoElement::DetailsContent
    }

    /// Whether the current pseudo element is ::before or ::after.
    #[inline]
    fn is_before_or_after(&self) -> bool {
        matches!(*self, PseudoElement::Before | PseudoElement::After)
    }
}

impl PseudoElement {
    /// Returns the kind of cascade type that a given pseudo is going to use.
    ///
    /// In Gecko we only compute ::before and ::after eagerly. We save the rules
    /// for anonymous boxes separately, so we resolve them as precomputed
    /// pseudos.
    ///
    /// We resolve the others lazily, see `Servo_ResolvePseudoStyle`.
    pub fn cascade_type(&self) -> PseudoElementCascadeType {
        if self.is_eager() {
            debug_assert!(!self.is_anon_box());
            return PseudoElementCascadeType::Eager;
        }

        if self.is_precomputed() {
            return PseudoElementCascadeType::Precomputed;
        }

        PseudoElementCascadeType::Lazy
    }

    /// Gets the canonical index of this eagerly-cascaded pseudo-element.
    #[inline]
    pub fn eager_index(&self) -> usize {
        EAGER_PSEUDOS
            .iter()
            .position(|p| p == self)
            .expect("Not an eager pseudo")
    }

    /// Creates a pseudo-element from an eager index.
    #[inline]
    pub fn from_eager_index(i: usize) -> Self {
        EAGER_PSEUDOS[i].clone()
    }

    /// Whether animations for the current pseudo element are stored in the
    /// parent element.
    #[inline]
    pub fn animations_stored_in_parent(&self) -> bool {
        matches!(*self, Self::Before | Self::After | Self::Marker | Self::Backdrop)
    }

    /// Whether this pseudo-element is the ::before pseudo.
    #[inline]
    pub fn is_before(&self) -> bool {
        *self == PseudoElement::Before
    }

    /// Whether this pseudo-element is the ::after pseudo.
    #[inline]
    pub fn is_after(&self) -> bool {
        *self == PseudoElement::After
    }

    /// Whether this pseudo-element is the ::marker pseudo.
    #[inline]
    pub fn is_marker(&self) -> bool {
        *self == PseudoElement::Marker
    }

    /// Whether this pseudo-element is the ::selection pseudo.
    #[inline]
    pub fn is_selection(&self) -> bool {
        *self == PseudoElement::Selection
    }

    /// Whether this pseudo-element is ::first-letter.
    #[inline]
    pub fn is_first_letter(&self) -> bool {
        *self == PseudoElement::FirstLetter
    }

    /// Whether this pseudo-element is ::first-line.
    #[inline]
    pub fn is_first_line(&self) -> bool {
        *self == PseudoElement::FirstLine
    }

    /// Whether this pseudo-element is the ::-moz-color-swatch pseudo.
    #[inline]
    pub fn is_color_swatch(&self) -> bool {
        *self == PseudoElement::MozColorSwatch
    }

    /// Whether this pseudo-element is lazily-cascaded.
    #[inline]
    pub fn is_lazy(&self) -> bool {
        !self.is_eager() && !self.is_precomputed()
    }

    /// The identifier of the highlight this pseudo-element represents.
    pub fn highlight_name(&self) -> Option<&AtomIdent> {
        match *self {
            Self::Highlight(ref name) => Some(name),
            _ => None,
        }
    }

    /// Whether this pseudo-element is the ::highlight pseudo.
    pub fn is_highlight(&self) -> bool {
        matches!(*self, Self::Highlight(_))
    }

    /// Whether this pseudo-element is the ::target-text pseudo.
    #[inline]
    pub fn is_target_text(&self) -> bool {
        *self == PseudoElement::TargetText
    }

    /// Whether this pseudo-element is a named view transition pseudo-element.
    pub fn is_named_view_transition(&self) -> bool {
        matches!(
            *self,
            Self::ViewTransitionGroup(..)
                | Self::ViewTransitionImagePair(..)
                | Self::ViewTransitionOld(..)
                | Self::ViewTransitionNew(..)
        )
    }

    /// The count we contribute to the specificity from this pseudo-element.
    pub fn specificity_count(&self) -> u32 {
        match *self {
            Self::ViewTransitionGroup(ref name_and_class)
            | Self::ViewTransitionImagePair(ref name_and_class)
            | Self::ViewTransitionOld(ref name_and_class)
            | Self::ViewTransitionNew(ref name_and_class) => {
                // The specificity of a named view transition pseudo-element selector with either:
                // 1. a <pt-name-selector> with a <custom-ident>; or
                // 2. a <pt-class-selector> with at least one <custom-ident>,
                // is equivalent to a type selector.
                //
                // The specificity of a named view transition pseudo-element selector with a `*`
                // argument and with an empty <pt-class-selector> is zero.
                // https://drafts.csswg.org/css-view-transitions-2/#pseudo-element-class-additions
                (name_and_class.name() != &atom!("*") || !name_and_class.classes().is_empty())
                    as u32
            },
            _ => 1,
        }
    }

    /// Whether this pseudo-element supports user action selectors.
    pub fn supports_user_action_state(&self) -> bool {
        (self.flags() & structs::CSS_PSEUDO_ELEMENT_SUPPORTS_USER_ACTION_STATE) != 0
    }

    /// Whether this pseudo-element is enabled for all content.
    pub fn enabled_in_content(&self) -> bool {
        match *self {
            Self::Highlight(..) => pref!("dom.customHighlightAPI.enabled"),
            Self::TargetText => pref!("dom.text_fragments.enabled"),
            Self::SliderFill | Self::SliderTrack | Self::SliderThumb => {
                pref!("layout.css.modern-range-pseudos.enabled")
            },
            Self::DetailsContent => {
                pref!("layout.css.details-content.enabled")
            },
            Self::ViewTransition
            | Self::ViewTransitionGroup(..)
            | Self::ViewTransitionImagePair(..)
            | Self::ViewTransitionOld(..)
            | Self::ViewTransitionNew(..) => pref!("dom.viewTransitions.enabled"),
            // If it's not explicitly enabled in UA sheets or chrome, then we're enabled for
            // content.
            _ => (self.flags() & structs::CSS_PSEUDO_ELEMENT_ENABLED_IN_UA_SHEETS_AND_CHROME) == 0,
        }
    }

    /// Whether this pseudo is enabled explicitly in UA sheets.
    pub fn enabled_in_ua_sheets(&self) -> bool {
        (self.flags() & structs::CSS_PSEUDO_ELEMENT_ENABLED_IN_UA_SHEETS) != 0
    }

    /// Whether this pseudo is enabled explicitly in chrome sheets.
    pub fn enabled_in_chrome(&self) -> bool {
        (self.flags() & structs::CSS_PSEUDO_ELEMENT_ENABLED_IN_CHROME) != 0
    }

    /// Whether this pseudo-element skips flex/grid container display-based
    /// fixup.
    #[inline]
    pub fn skip_item_display_fixup(&self) -> bool {
        (self.flags() & structs::CSS_PSEUDO_ELEMENT_IS_FLEX_OR_GRID_ITEM) == 0
    }

    /// Whether this pseudo-element is precomputed.
    #[inline]
    pub fn is_precomputed(&self) -> bool {
        self.is_anon_box() && !self.is_tree_pseudo_element()
    }

    /// Property flag that properties must have to apply to this pseudo-element.
    #[inline]
    pub fn property_restriction(&self) -> Option<PropertyFlags> {
        Some(match *self {
            PseudoElement::FirstLetter => PropertyFlags::APPLIES_TO_FIRST_LETTER,
            PseudoElement::FirstLine => PropertyFlags::APPLIES_TO_FIRST_LINE,
            PseudoElement::Placeholder => PropertyFlags::APPLIES_TO_PLACEHOLDER,
            PseudoElement::Cue => PropertyFlags::APPLIES_TO_CUE,
            PseudoElement::Marker if static_prefs::pref!("layout.css.marker.restricted") => {
                PropertyFlags::APPLIES_TO_MARKER
            },
            _ => return None,
        })
    }

    /// Whether this pseudo-element should actually exist if it has
    /// the given styles.
    pub fn should_exist(&self, style: &ComputedValues) -> bool {
        debug_assert!(self.is_eager());

        if style.get_box().clone_display() == Display::None {
            return false;
        }

        if self.is_before_or_after() && style.ineffective_content_property() {
            return false;
        }

        true
    }

    /// Parse the pseudo-element string without the check of enabled state. This may includes
    /// all possible PseudoElement, including tree pseudo-elements and anonymous box.
    // TODO: Bug 1845712. Merge this with the pseudo element part in parse_one_simple_selector().
    pub fn parse_ignore_enabled_state<'i, 't>(
        input: &mut Parser<'i, 't>,
    ) -> Result<Self, ParseError<'i>> {
        use crate::gecko::selector_parser;
        use cssparser::Token;
        use selectors::parser::{is_css2_pseudo_element, SelectorParseErrorKind};
        use style_traits::StyleParseErrorKind;

        // The pseudo-element string should start with ':'.
        input.expect_colon()?;

        let location = input.current_source_location();
        let next = input.next_including_whitespace()?;
        if !matches!(next, Token::Colon) {
            // Parse a CSS2 pseudo-element.
            let name = match next {
                Token::Ident(name) if is_css2_pseudo_element(&name) => name,
                _ => return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)),
            };
            return PseudoElement::from_slice(&name, false).ok_or(location.new_custom_error(
                SelectorParseErrorKind::UnsupportedPseudoClassOrElement(name.clone()),
            ));
        }

        // Now we have double colons, so check the following tokens.
        match input.next_including_whitespace()?.clone() {
            Token::Ident(name) => {
                // We don't need to parse unknown ::-webkit-* pseudo-elements in this function.
                PseudoElement::from_slice(&name, false).ok_or(input.new_custom_error(
                    SelectorParseErrorKind::UnsupportedPseudoClassOrElement(name),
                ))
            },
            Token::Function(name) => {
                // Note: ::slotted() and ::part() are not accepted in getComputedStyle().
                // https://drafts.csswg.org/cssom/#dom-window-getcomputedstyle
                input.parse_nested_block(|input| {
                    selector_parser::parse_functional_pseudo_element_with_name(
                        name,
                        input,
                        Target::Cssom,
                    )
                })
            },
            t => return Err(input.new_unexpected_token_error(t)),
        }
    }

    /// Returns true if this pseudo-element matches its selector.
    pub fn matches_named_view_transition_pseudo_element(
        &self,
        selector: &Self,
        element: &super::wrapper::GeckoElement,
    ) -> bool {
        use crate::gecko_bindings::bindings;

        match (self, selector) {
            (
                &Self::ViewTransitionGroup(ref name),
                &Self::ViewTransitionGroup(ref s_name_class),
            )
            | (
                &Self::ViewTransitionImagePair(ref name),
                &Self::ViewTransitionImagePair(ref s_name_class),
            )
            | (&Self::ViewTransitionOld(ref name), &Self::ViewTransitionOld(ref s_name_class))
            | (&Self::ViewTransitionNew(ref name), &Self::ViewTransitionNew(ref s_name_class)) => {
                // Named view transition pseudos accept the universal selector as the name, so we
                // check it first.
                // https://drafts.csswg.org/css-view-transitions-1/#named-view-transition-pseudo
                let s_name = s_name_class.name();
                if s_name != name.name() && s_name != &atom!("*") {
                    return false;
                }

                // We have to check class list only when the name is matched and there are one or
                // more <pt-class-selector>s.
                s_name_class.classes().is_empty()
                    || unsafe {
                        bindings::Gecko_MatchViewTransitionClass(
                            element.0,
                            s_name_class.name_and_classes(),
                        )
                    }
            },
            _ => false,
        }
    }
}
