/* 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-specific bits for selector-parsing.

use crate::computed_value_flags::ComputedValueFlags;
use crate::invalidation::element::document_state::InvalidationMatchingData;
use crate::properties::ComputedValues;
use crate::selector_parser::{Direction, HorizontalDirection, SelectorParser};
use crate::str::starts_with_ignore_ascii_case;
use crate::string_cache::{Atom, Namespace, WeakAtom, WeakNamespace};
use crate::values::{AtomIdent, AtomString};
use cssparser::{BasicParseError, BasicParseErrorKind, Parser};
use cssparser::{CowRcStr, SourceLocation, ToCss, Token};
use dom::{DocumentState, ElementState};
use selectors::parser::SelectorParseErrorKind;
use std::fmt;
use style_traits::{CssWriter, ParseError, StyleParseErrorKind, ToCss as ToCss_};
use thin_vec::ThinVec;

pub use crate::gecko::pseudo_element::{
    PseudoElement, EAGER_PSEUDOS, EAGER_PSEUDO_COUNT, PSEUDO_COUNT,
};
pub use crate::gecko::snapshot::SnapshotMap;

bitflags! {
    // See NonTSPseudoClass::is_enabled_in()
    #[derive(Copy, Clone)]
    struct NonTSPseudoClassFlag: u8 {
        const PSEUDO_CLASS_ENABLED_IN_UA_SHEETS = 1 << 0;
        const PSEUDO_CLASS_ENABLED_IN_CHROME = 1 << 1;
        const PSEUDO_CLASS_ENABLED_IN_UA_SHEETS_AND_CHROME =
            NonTSPseudoClassFlag::PSEUDO_CLASS_ENABLED_IN_UA_SHEETS.bits |
            NonTSPseudoClassFlag::PSEUDO_CLASS_ENABLED_IN_CHROME.bits;
    }
}

/// The type used to store the language argument to the `:lang` pseudo-class.
#[derive(Clone, Debug, Eq, MallocSizeOf, PartialEq, ToCss, ToShmem)]
#[css(comma)]
pub struct Lang(#[css(iterable)] pub ThinVec<AtomIdent>);

macro_rules! pseudo_class_name {
    ([$(($css:expr, $name:ident, $state:tt, $flags:tt),)*]) => {
        /// Our representation of a non tree-structural pseudo-class.
        #[derive(Clone, Debug, Eq, MallocSizeOf, PartialEq, ToShmem)]
        pub enum NonTSPseudoClass {
            $(
                #[doc = $css]
                $name,
            )*
            /// The `:lang` pseudo-class.
            Lang(Lang),
            /// The `:dir` pseudo-class.
            Dir(Direction),
            /// The non-standard `:-moz-locale-dir` pseudo-class.
            MozLocaleDir(Direction),
        }
    }
}
apply_non_ts_list!(pseudo_class_name);

impl ToCss for NonTSPseudoClass {
    fn to_css<W>(&self, dest: &mut W) -> fmt::Result
    where
        W: fmt::Write,
    {
        macro_rules! pseudo_class_serialize {
            ([$(($css:expr, $name:ident, $state:tt, $flags:tt),)*]) => {
                match *self {
                    $(NonTSPseudoClass::$name => concat!(":", $css),)*
                    NonTSPseudoClass::Lang(ref lang) => {
                        dest.write_str(":lang(")?;
                        lang.to_css(&mut CssWriter::new(dest))?;
                        return dest.write_char(')');
                    },
                    NonTSPseudoClass::MozLocaleDir(ref dir) => {
                        dest.write_str(":-moz-locale-dir(")?;
                        dir.to_css(&mut CssWriter::new(dest))?;
                        return dest.write_char(')')
                    },
                    NonTSPseudoClass::Dir(ref dir) => {
                        dest.write_str(":dir(")?;
                        dir.to_css(&mut CssWriter::new(dest))?;
                        return dest.write_char(')')
                    },
                }
            }
        }
        let ser = apply_non_ts_list!(pseudo_class_serialize);
        dest.write_str(ser)
    }
}

impl NonTSPseudoClass {
    /// Parses the name and returns a non-ts-pseudo-class if succeeds.
    /// None otherwise. It doesn't check whether the pseudo-class is enabled
    /// in a particular state.
    pub fn parse_non_functional(name: &str) -> Option<Self> {
        macro_rules! pseudo_class_parse {
            ([$(($css:expr, $name:ident, $state:tt, $flags:tt),)*]) => {
                match_ignore_ascii_case! { &name,
                    $($css => Some(NonTSPseudoClass::$name),)*
                    "-moz-full-screen" => Some(NonTSPseudoClass::Fullscreen),
                    "-moz-read-only" => Some(NonTSPseudoClass::ReadOnly),
                    "-moz-read-write" => Some(NonTSPseudoClass::ReadWrite),
                    "-moz-focusring" => Some(NonTSPseudoClass::FocusVisible),
                    "-moz-ui-valid" => Some(NonTSPseudoClass::UserValid),
                    "-moz-ui-invalid" => Some(NonTSPseudoClass::UserInvalid),
                    "-webkit-autofill" => Some(NonTSPseudoClass::Autofill),
                    _ => None,
                }
            }
        }
        apply_non_ts_list!(pseudo_class_parse)
    }

    /// Returns true if this pseudo-class has any of the given flags set.
    fn has_any_flag(&self, flags: NonTSPseudoClassFlag) -> bool {
        macro_rules! check_flag {
            (_) => {
                false
            };
            ($flags:ident) => {
                NonTSPseudoClassFlag::$flags.intersects(flags)
            };
        }
        macro_rules! pseudo_class_check_is_enabled_in {
            ([$(($css:expr, $name:ident, $state:tt, $flags:tt),)*]) => {
                match *self {
                    $(NonTSPseudoClass::$name => check_flag!($flags),)*
                    NonTSPseudoClass::MozLocaleDir(_) => check_flag!(PSEUDO_CLASS_ENABLED_IN_UA_SHEETS_AND_CHROME),
                    NonTSPseudoClass::Lang(_) |
                    NonTSPseudoClass::Dir(_) => false,
                }
            }
        }
        apply_non_ts_list!(pseudo_class_check_is_enabled_in)
    }

    /// Returns whether the pseudo-class is enabled in content sheets.
    #[inline]
    fn is_enabled_in_content(&self) -> bool {
        if matches!(*self, Self::PopoverOpen) {
            return static_prefs::pref!("dom.element.popover.enabled");
        }
        !self.has_any_flag(NonTSPseudoClassFlag::PSEUDO_CLASS_ENABLED_IN_UA_SHEETS_AND_CHROME)
    }

    /// Get the state flag associated with a pseudo-class, if any.
    pub fn state_flag(&self) -> ElementState {
        macro_rules! flag {
            (_) => {
                ElementState::empty()
            };
            ($state:ident) => {
                ElementState::$state
            };
        }
        macro_rules! pseudo_class_state {
            ([$(($css:expr, $name:ident, $state:tt, $flags:tt),)*]) => {
                match *self {
                    $(NonTSPseudoClass::$name => flag!($state),)*
                    NonTSPseudoClass::Dir(ref dir) => dir.element_state(),
                    NonTSPseudoClass::MozLocaleDir(..) |
                    NonTSPseudoClass::Lang(..) => ElementState::empty(),
                }
            }
        }
        apply_non_ts_list!(pseudo_class_state)
    }

    /// Get the document state flag associated with a pseudo-class, if any.
    pub fn document_state_flag(&self) -> DocumentState {
        match *self {
            NonTSPseudoClass::MozLocaleDir(ref dir) => match dir.as_horizontal_direction() {
                Some(HorizontalDirection::Ltr) => DocumentState::LTR_LOCALE,
                Some(HorizontalDirection::Rtl) => DocumentState::RTL_LOCALE,
                None => DocumentState::empty(),
            },
            NonTSPseudoClass::MozWindowInactive => DocumentState::WINDOW_INACTIVE,
            NonTSPseudoClass::MozLWTheme => DocumentState::LWTHEME,
            _ => DocumentState::empty(),
        }
    }

    /// Returns true if the given pseudoclass should trigger style sharing cache
    /// revalidation.
    pub fn needs_cache_revalidation(&self) -> bool {
        self.state_flag().is_empty() &&
            !matches!(
                *self,
                // :dir() depends on state only, but may have an empty state_flag for invalid
                // arguments.
                NonTSPseudoClass::Dir(_) |
                      // We prevent style sharing for NAC.
                      NonTSPseudoClass::MozNativeAnonymous |
                      // :-moz-placeholder is parsed but never matches.
                      NonTSPseudoClass::MozPlaceholder |
                      // :-moz-is-html, :-moz-lwtheme, :-moz-locale-dir and :-moz-window-inactive
                      // depend only on the state of the document, which is invariant across all
                      // elements involved in a given style cache.
                      NonTSPseudoClass::MozIsHTML |
                      NonTSPseudoClass::MozLWTheme |
                      NonTSPseudoClass::MozLocaleDir(_) |
                      NonTSPseudoClass::MozWindowInactive
            )
    }
}

impl ::selectors::parser::NonTSPseudoClass for NonTSPseudoClass {
    type Impl = SelectorImpl;

    #[inline]
    fn is_active_or_hover(&self) -> bool {
        matches!(*self, NonTSPseudoClass::Active | NonTSPseudoClass::Hover)
    }

    /// We intentionally skip the link-related ones.
    #[inline]
    fn is_user_action_state(&self) -> bool {
        matches!(
            *self,
            NonTSPseudoClass::Hover | NonTSPseudoClass::Active | NonTSPseudoClass::Focus
        )
    }
}

/// The dummy struct we use to implement our selector parsing.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct SelectorImpl;

/// A set of extra data to carry along with the matching context, either for
/// selector-matching or invalidation.
#[derive(Default)]
pub struct ExtraMatchingData<'a> {
    /// The invalidation data to invalidate doc-state pseudo-classes correctly.
    pub invalidation_data: InvalidationMatchingData,

    /// The invalidation bits from matching container queries. These are here
    /// just for convenience mostly.
    pub cascade_input_flags: ComputedValueFlags,

    /// The style of the originating element in order to evaluate @container
    /// size queries affecting pseudo-elements.
    pub originating_element_style: Option<&'a ComputedValues>,
}

impl ::selectors::SelectorImpl for SelectorImpl {
    type ExtraMatchingData<'a> = ExtraMatchingData<'a>;
    type AttrValue = AtomString;
    type Identifier = AtomIdent;
    type LocalName = AtomIdent;
    type NamespacePrefix = AtomIdent;
    type NamespaceUrl = Namespace;
    type BorrowedNamespaceUrl = WeakNamespace;
    type BorrowedLocalName = WeakAtom;

    type PseudoElement = PseudoElement;
    type NonTSPseudoClass = NonTSPseudoClass;

    fn should_collect_attr_hash(name: &AtomIdent) -> bool {
        !crate::bloom::is_attr_name_excluded_from_filter(name)
    }
}

impl<'a> SelectorParser<'a> {
    fn is_pseudo_class_enabled(&self, pseudo_class: &NonTSPseudoClass) -> bool {
        if pseudo_class.is_enabled_in_content() {
            return true;
        }

        if self.in_user_agent_stylesheet() &&
            pseudo_class.has_any_flag(NonTSPseudoClassFlag::PSEUDO_CLASS_ENABLED_IN_UA_SHEETS)
        {
            return true;
        }

        if self.chrome_rules_enabled() &&
            pseudo_class.has_any_flag(NonTSPseudoClassFlag::PSEUDO_CLASS_ENABLED_IN_CHROME)
        {
            return true;
        }

        return false;
    }

    fn is_pseudo_element_enabled(&self, pseudo_element: &PseudoElement) -> bool {
        if pseudo_element.enabled_in_content() {
            return true;
        }

        if self.in_user_agent_stylesheet() && pseudo_element.enabled_in_ua_sheets() {
            return true;
        }

        if self.chrome_rules_enabled() && pseudo_element.enabled_in_chrome() {
            return true;
        }

        return false;
    }
}

impl<'a, 'i> ::selectors::Parser<'i> for SelectorParser<'a> {
    type Impl = SelectorImpl;
    type Error = StyleParseErrorKind<'i>;

    fn parse_parent_selector(&self) -> bool {
        static_prefs::pref!("layout.css.nesting.enabled")
    }

    #[inline]
    fn parse_slotted(&self) -> bool {
        true
    }

    #[inline]
    fn parse_host(&self) -> bool {
        true
    }

    #[inline]
    fn parse_nth_child_of(&self) -> bool {
        true
    }

    #[inline]
    fn parse_is_and_where(&self) -> bool {
        true
    }

    #[inline]
    fn parse_has(&self) -> bool {
        static_prefs::pref!("layout.css.has-selector.enabled")
    }

    #[inline]
    fn parse_part(&self) -> bool {
        true
    }

    #[inline]
    fn is_is_alias(&self, function: &str) -> bool {
        function.eq_ignore_ascii_case("-moz-any")
    }

    #[inline]
    fn allow_forgiving_selectors(&self) -> bool {
        !self.for_supports_rule
    }

    fn parse_non_ts_pseudo_class(
        &self,
        location: SourceLocation,
        name: CowRcStr<'i>,
    ) -> Result<NonTSPseudoClass, ParseError<'i>> {
        if let Some(pseudo_class) = NonTSPseudoClass::parse_non_functional(&name) {
            if self.is_pseudo_class_enabled(&pseudo_class) {
                return Ok(pseudo_class);
            }
        }
        Err(
            location.new_custom_error(SelectorParseErrorKind::UnsupportedPseudoClassOrElement(
                name,
            )),
        )
    }

    fn parse_non_ts_functional_pseudo_class<'t>(
        &self,
        name: CowRcStr<'i>,
        parser: &mut Parser<'i, 't>,
    ) -> Result<NonTSPseudoClass, ParseError<'i>> {
        let pseudo_class = match_ignore_ascii_case! { &name,
            "lang" => {
                let result = parser.parse_comma_separated(|input| {
                    Ok(AtomIdent::from(input.expect_ident_or_string()?.as_ref()))
                })?;
                if result.is_empty() {
                    return Err(parser.new_custom_error(StyleParseErrorKind::UnspecifiedError));
                }
                NonTSPseudoClass::Lang(Lang(result.into()))
            },
            "-moz-locale-dir" => {
                NonTSPseudoClass::MozLocaleDir(Direction::parse(parser)?)
            },
            "dir" => {
                NonTSPseudoClass::Dir(Direction::parse(parser)?)
            },
            _ => return Err(parser.new_custom_error(
                SelectorParseErrorKind::UnsupportedPseudoClassOrElement(name.clone())
            ))
        };
        if self.is_pseudo_class_enabled(&pseudo_class) {
            Ok(pseudo_class)
        } else {
            Err(
                parser.new_custom_error(SelectorParseErrorKind::UnsupportedPseudoClassOrElement(
                    name,
                )),
            )
        }
    }

    fn parse_pseudo_element(
        &self,
        location: SourceLocation,
        name: CowRcStr<'i>,
    ) -> Result<PseudoElement, ParseError<'i>> {
        let allow_unkown_webkit = !self.for_supports_rule;
        if let Some(pseudo) = PseudoElement::from_slice(&name, allow_unkown_webkit) {
            if self.is_pseudo_element_enabled(&pseudo) {
                return Ok(pseudo);
            }
        }

        Err(
            location.new_custom_error(SelectorParseErrorKind::UnsupportedPseudoClassOrElement(
                name,
            )),
        )
    }

    fn parse_functional_pseudo_element<'t>(
        &self,
        name: CowRcStr<'i>,
        parser: &mut Parser<'i, 't>,
    ) -> Result<PseudoElement, ParseError<'i>> {
        if starts_with_ignore_ascii_case(&name, "-moz-tree-") {
            // Tree pseudo-elements can have zero or more arguments, separated
            // by either comma or space.
            let mut args = ThinVec::new();
            loop {
                let location = parser.current_source_location();
                match parser.next() {
                    Ok(&Token::Ident(ref ident)) => args.push(Atom::from(ident.as_ref())),
                    Ok(&Token::Comma) => {},
                    Ok(t) => return Err(location.new_unexpected_token_error(t.clone())),
                    Err(BasicParseError {
                        kind: BasicParseErrorKind::EndOfInput,
                        ..
                    }) => break,
                    _ => unreachable!("Parser::next() shouldn't return any other error"),
                }
            }
            if let Some(pseudo) = PseudoElement::tree_pseudo_element(&name, args) {
                if self.is_pseudo_element_enabled(&pseudo) {
                    return Ok(pseudo);
                }
            }
        } else if name.eq_ignore_ascii_case("highlight") {
            let pseudo = PseudoElement::Highlight(AtomIdent::from(parser.expect_ident()?.as_ref()));
            if self.is_pseudo_element_enabled(&pseudo) {
                return Ok(pseudo);
            }
        }
        Err(
            parser.new_custom_error(SelectorParseErrorKind::UnsupportedPseudoClassOrElement(
                name,
            )),
        )
    }

    fn default_namespace(&self) -> Option<Namespace> {
        self.namespaces.default.clone()
    }

    fn namespace_for_prefix(&self, prefix: &AtomIdent) -> Option<Namespace> {
        self.namespaces.prefixes.get(prefix).cloned()
    }
}

impl SelectorImpl {
    /// A helper to traverse each eagerly cascaded pseudo-element, executing
    /// `fun` on it.
    #[inline]
    pub fn each_eagerly_cascaded_pseudo_element<F>(mut fun: F)
    where
        F: FnMut(PseudoElement),
    {
        for pseudo in &EAGER_PSEUDOS {
            fun(pseudo.clone())
        }
    }
}

// Selector and component sizes are important for matching performance.
size_of_test!(selectors::parser::Selector<SelectorImpl>, 8);
size_of_test!(selectors::parser::Component<SelectorImpl>, 24);
size_of_test!(PseudoElement, 16);
size_of_test!(NonTSPseudoClass, 16);
