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

//! Types used to report parsing errors.

#![deny(missing_docs)]

use crate::selector_parser::SelectorImpl;
use crate::stylesheets::UrlExtraData;
use cssparser::{BasicParseErrorKind, ParseErrorKind, SourceLocation, Token};
use selectors::parser::{Combinator, Component, RelativeSelector, Selector};
use selectors::visitor::{SelectorListKind, SelectorVisitor};
use selectors::SelectorList;
use std::fmt;
use style_traits::ParseError;

/// Errors that can be encountered while parsing CSS.
#[derive(Debug)]
pub enum ContextualParseError<'a> {
    /// A property declaration was not recognized.
    UnsupportedPropertyDeclaration(&'a str, ParseError<'a>, &'a [SelectorList<SelectorImpl>]),
    /// A property descriptor was not recognized.
    UnsupportedPropertyDescriptor(&'a str, ParseError<'a>),
    /// A font face descriptor was not recognized.
    UnsupportedFontFaceDescriptor(&'a str, ParseError<'a>),
    /// A font feature values descriptor was not recognized.
    UnsupportedFontFeatureValuesDescriptor(&'a str, ParseError<'a>),
    /// A font palette values descriptor was not recognized.
    UnsupportedFontPaletteValuesDescriptor(&'a str, ParseError<'a>),
    /// A keyframe rule was not valid.
    InvalidKeyframeRule(&'a str, ParseError<'a>),
    /// A font feature values rule was not valid.
    InvalidFontFeatureValuesRule(&'a str, ParseError<'a>),
    /// A rule was invalid for some reason.
    InvalidRule(&'a str, ParseError<'a>),
    /// A rule was not recognized.
    UnsupportedRule(&'a str, ParseError<'a>),
    /// A viewport descriptor declaration was not recognized.
    UnsupportedViewportDescriptorDeclaration(&'a str, ParseError<'a>),
    /// A counter style descriptor declaration was not recognized.
    UnsupportedCounterStyleDescriptorDeclaration(&'a str, ParseError<'a>),
    /// A counter style rule had no symbols.
    InvalidCounterStyleWithoutSymbols(String),
    /// A counter style rule had less than two symbols.
    InvalidCounterStyleNotEnoughSymbols(String),
    /// A counter style rule did not have additive-symbols.
    InvalidCounterStyleWithoutAdditiveSymbols,
    /// A counter style rule had extends with symbols.
    InvalidCounterStyleExtendsWithSymbols,
    /// A counter style rule had extends with additive-symbols.
    InvalidCounterStyleExtendsWithAdditiveSymbols,
    /// A media rule was invalid for some reason.
    InvalidMediaRule(&'a str, ParseError<'a>),
    /// A value was not recognized.
    UnsupportedValue(&'a str, ParseError<'a>),
    /// A never-matching `:host` selector was found.
    NeverMatchingHostSelector(String),
}

impl<'a> fmt::Display for ContextualParseError<'a> {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        fn token_to_str(t: &Token, f: &mut fmt::Formatter) -> fmt::Result {
            match *t {
                Token::Ident(ref i) => write!(f, "identifier {}", i),
                Token::AtKeyword(ref kw) => write!(f, "keyword @{}", kw),
                Token::Hash(ref h) => write!(f, "hash #{}", h),
                Token::IDHash(ref h) => write!(f, "id selector #{}", h),
                Token::QuotedString(ref s) => write!(f, "quoted string \"{}\"", s),
                Token::UnquotedUrl(ref u) => write!(f, "url {}", u),
                Token::Delim(ref d) => write!(f, "delimiter {}", d),
                Token::Number {
                    int_value: Some(i), ..
                } => write!(f, "number {}", i),
                Token::Number { value, .. } => write!(f, "number {}", value),
                Token::Percentage {
                    int_value: Some(i), ..
                } => write!(f, "percentage {}", i),
                Token::Percentage { unit_value, .. } => {
                    write!(f, "percentage {}", unit_value * 100.)
                },
                Token::Dimension {
                    value, ref unit, ..
                } => write!(f, "dimension {}{}", value, unit),
                Token::WhiteSpace(_) => write!(f, "whitespace"),
                Token::Comment(_) => write!(f, "comment"),
                Token::Colon => write!(f, "colon (:)"),
                Token::Semicolon => write!(f, "semicolon (;)"),
                Token::Comma => write!(f, "comma (,)"),
                Token::IncludeMatch => write!(f, "include match (~=)"),
                Token::DashMatch => write!(f, "dash match (|=)"),
                Token::PrefixMatch => write!(f, "prefix match (^=)"),
                Token::SuffixMatch => write!(f, "suffix match ($=)"),
                Token::SubstringMatch => write!(f, "substring match (*=)"),
                Token::CDO => write!(f, "CDO (<!--)"),
                Token::CDC => write!(f, "CDC (-->)"),
                Token::Function(ref name) => write!(f, "function {}", name),
                Token::ParenthesisBlock => write!(f, "parenthesis ("),
                Token::SquareBracketBlock => write!(f, "square bracket ["),
                Token::CurlyBracketBlock => write!(f, "curly bracket {{"),
                Token::BadUrl(ref _u) => write!(f, "bad url parse error"),
                Token::BadString(ref _s) => write!(f, "bad string parse error"),
                Token::CloseParenthesis => write!(f, "unmatched close parenthesis"),
                Token::CloseSquareBracket => write!(f, "unmatched close square bracket"),
                Token::CloseCurlyBracket => write!(f, "unmatched close curly bracket"),
            }
        }

        fn parse_error_to_str(err: &ParseError, f: &mut fmt::Formatter) -> fmt::Result {
            match err.kind {
                ParseErrorKind::Basic(BasicParseErrorKind::UnexpectedToken(ref t)) => {
                    write!(f, "found unexpected ")?;
                    token_to_str(t, f)
                },
                ParseErrorKind::Basic(BasicParseErrorKind::EndOfInput) => {
                    write!(f, "unexpected end of input")
                },
                ParseErrorKind::Basic(BasicParseErrorKind::AtRuleInvalid(ref i)) => {
                    write!(f, "@ rule invalid: {}", i)
                },
                ParseErrorKind::Basic(BasicParseErrorKind::AtRuleBodyInvalid) => {
                    write!(f, "@ rule invalid")
                },
                ParseErrorKind::Basic(BasicParseErrorKind::QualifiedRuleInvalid) => {
                    write!(f, "qualified rule invalid")
                },
                ParseErrorKind::Custom(ref err) => write!(f, "{:?}", err),
            }
        }

        match *self {
            ContextualParseError::UnsupportedPropertyDeclaration(decl, ref err, _selectors) => {
                write!(f, "Unsupported property declaration: '{}', ", decl)?;
                parse_error_to_str(err, f)
            },
            ContextualParseError::UnsupportedPropertyDescriptor(decl, ref err) => {
                write!(
                    f,
                    "Unsupported @property descriptor declaration: '{}', ",
                    decl
                )?;
                parse_error_to_str(err, f)
            },
            ContextualParseError::UnsupportedFontFaceDescriptor(decl, ref err) => {
                write!(
                    f,
                    "Unsupported @font-face descriptor declaration: '{}', ",
                    decl
                )?;
                parse_error_to_str(err, f)
            },
            ContextualParseError::UnsupportedFontFeatureValuesDescriptor(decl, ref err) => {
                write!(
                    f,
                    "Unsupported @font-feature-values descriptor declaration: '{}', ",
                    decl
                )?;
                parse_error_to_str(err, f)
            },
            ContextualParseError::UnsupportedFontPaletteValuesDescriptor(decl, ref err) => {
                write!(
                    f,
                    "Unsupported @font-palette-values descriptor declaration: '{}', ",
                    decl
                )?;
                parse_error_to_str(err, f)
            },
            ContextualParseError::InvalidKeyframeRule(rule, ref err) => {
                write!(f, "Invalid keyframe rule: '{}', ", rule)?;
                parse_error_to_str(err, f)
            },
            ContextualParseError::InvalidFontFeatureValuesRule(rule, ref err) => {
                write!(f, "Invalid font feature value rule: '{}', ", rule)?;
                parse_error_to_str(err, f)
            },
            ContextualParseError::InvalidRule(rule, ref err) => {
                write!(f, "Invalid rule: '{}', ", rule)?;
                parse_error_to_str(err, f)
            },
            ContextualParseError::UnsupportedRule(rule, ref err) => {
                write!(f, "Unsupported rule: '{}', ", rule)?;
                parse_error_to_str(err, f)
            },
            ContextualParseError::UnsupportedViewportDescriptorDeclaration(decl, ref err) => {
                write!(
                    f,
                    "Unsupported @viewport descriptor declaration: '{}', ",
                    decl
                )?;
                parse_error_to_str(err, f)
            },
            ContextualParseError::UnsupportedCounterStyleDescriptorDeclaration(decl, ref err) => {
                write!(
                    f,
                    "Unsupported @counter-style descriptor declaration: '{}', ",
                    decl
                )?;
                parse_error_to_str(err, f)
            },
            ContextualParseError::InvalidCounterStyleWithoutSymbols(ref system) => write!(
                f,
                "Invalid @counter-style rule: 'system: {}' without 'symbols'",
                system
            ),
            ContextualParseError::InvalidCounterStyleNotEnoughSymbols(ref system) => write!(
                f,
                "Invalid @counter-style rule: 'system: {}' less than two 'symbols'",
                system
            ),
            ContextualParseError::InvalidCounterStyleWithoutAdditiveSymbols => write!(
                f,
                "Invalid @counter-style rule: 'system: additive' without 'additive-symbols'"
            ),
            ContextualParseError::InvalidCounterStyleExtendsWithSymbols => write!(
                f,
                "Invalid @counter-style rule: 'system: extends …' with 'symbols'"
            ),
            ContextualParseError::InvalidCounterStyleExtendsWithAdditiveSymbols => write!(
                f,
                "Invalid @counter-style rule: 'system: extends …' with 'additive-symbols'"
            ),
            ContextualParseError::InvalidMediaRule(media_rule, ref err) => {
                write!(f, "Invalid media rule: {}, ", media_rule)?;
                parse_error_to_str(err, f)
            },
            ContextualParseError::UnsupportedValue(_value, ref err) => parse_error_to_str(err, f),
            ContextualParseError::NeverMatchingHostSelector(ref selector) => {
                write!(f, ":host selector is not featureless: {}", selector)
            },
        }
    }
}

/// A generic trait for an error reporter.
pub trait ParseErrorReporter {
    /// Called when the style engine detects an error.
    ///
    /// Returns the current input being parsed, the source location it was
    /// reported from, and a message.
    fn report_error(
        &self,
        url: &UrlExtraData,
        location: SourceLocation,
        error: ContextualParseError,
    );
}

/// An error reporter that uses [the `log` crate](https://github.com/rust-lang-nursery/log)
/// at `info` level.
///
/// This logging is silent by default, and can be enabled with a `RUST_LOG=style=info`
/// environment variable.
/// (See [`env_logger`](https://rust-lang-nursery.github.io/log/env_logger/).)
#[cfg(feature = "servo")]
pub struct RustLogReporter;

#[cfg(feature = "servo")]
impl ParseErrorReporter for RustLogReporter {
    fn report_error(
        &self,
        url: &UrlExtraData,
        location: SourceLocation,
        error: ContextualParseError,
    ) {
        if log_enabled!(log::Level::Info) {
            info!(
                "Url:\t{}\n{}:{} {}",
                url.as_str(),
                location.line,
                location.column,
                error
            )
        }
    }
}

/// Any warning a selector may generate.
/// TODO(dshin): Bug 1860634 - Merge with never matching host selector warning, which is part of the rule parser.
#[repr(u8)]
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum SelectorWarningKind {
    /// Relative Selector with not enough constraint, either outside or inside the selector. e.g. `*:has(.a)`, `.a:has(*)`.
    /// May cause expensive invalidations for every element inserted and/or removed.
    UnconstraintedRelativeSelector,
    /// `:scope` can have 3 meanings, but in all cases, the relationship is defined strictly by an ancestor-descendant
    /// relationship. This means that any presence of sibling selectors to its right would make it never match.
    SiblingCombinatorAfterScopeSelector,
}

impl SelectorWarningKind {
    /// Get all warnings for this selector.
    pub fn from_selector(selector: &Selector<SelectorImpl>) -> Vec<Self> {
        let mut result = vec![];
        if UnconstrainedRelativeSelectorVisitor::has_warning(selector, 0, false) {
            result.push(SelectorWarningKind::UnconstraintedRelativeSelector);
        }
        if SiblingCombinatorAfterScopeSelectorVisitor::has_warning(selector) {
            result.push(SelectorWarningKind::SiblingCombinatorAfterScopeSelector);
        }
        result
    }
}

/// Per-compound state for finding unconstrained relative selectors.
struct PerCompoundState {
    /// Is there a relative selector in this compound?
    relative_selector_found: bool,
    /// Is this compound constrained in any way?
    constrained: bool,
    /// Nested below, or inside relative selector?
    in_relative_selector: bool,
}

impl PerCompoundState {
    fn new(in_relative_selector: bool) -> Self {
        Self {
            relative_selector_found: false,
            constrained: false,
            in_relative_selector,
        }
    }
}

/// Visitor to check if there's any unconstrained relative selector.
struct UnconstrainedRelativeSelectorVisitor {
    compound_state: PerCompoundState,
}

impl UnconstrainedRelativeSelectorVisitor {
    fn new(in_relative_selector: bool) -> Self {
        Self {
            compound_state: PerCompoundState::new(in_relative_selector),
        }
    }

    fn has_warning(
        selector: &Selector<SelectorImpl>,
        offset: usize,
        in_relative_selector: bool,
    ) -> bool {
        let relative_selector = matches!(
            selector.iter_raw_parse_order_from(0).next().unwrap(),
            Component::RelativeSelectorAnchor
        );
        debug_assert!(
            !relative_selector || offset == 0,
            "Checking relative selector from non-rightmost?"
        );
        let mut visitor = Self::new(in_relative_selector);
        let mut iter = if relative_selector {
            selector.iter_skip_relative_selector_anchor()
        } else {
            selector.iter_from(offset)
        };
        loop {
            visitor.compound_state = PerCompoundState::new(in_relative_selector);

            for s in &mut iter {
                s.visit(&mut visitor);
            }

            if (visitor.compound_state.relative_selector_found
                || visitor.compound_state.in_relative_selector)
                && !visitor.compound_state.constrained
            {
                return true;
            }

            if iter.next_sequence().is_none() {
                break;
            }
        }
        false
    }
}

impl SelectorVisitor for UnconstrainedRelativeSelectorVisitor {
    type Impl = SelectorImpl;

    fn visit_simple_selector(&mut self, c: &Component<Self::Impl>) -> bool {
        match c {
            // Deferred to visit_selector_list
            Component::Is(..)
            | Component::Where(..)
            | Component::Negation(..)
            | Component::Has(..) => (),
            Component::ExplicitUniversalType => (),
            _ => self.compound_state.constrained |= true,
        };
        true
    }

    fn visit_selector_list(
        &mut self,
        _list_kind: SelectorListKind,
        list: &[Selector<Self::Impl>],
    ) -> bool {
        let mut all_constrained = true;
        for s in list {
            let mut offset = 0;
            // First, check the rightmost compound for constraint at this level.
            if !self.compound_state.in_relative_selector {
                let mut nested = Self::new(false);
                let mut iter = s.iter();
                loop {
                    for c in &mut iter {
                        c.visit(&mut nested);
                        offset += 1;
                    }

                    let c = iter.next_sequence();
                    offset += 1;
                    if c.map_or(true, |c| !c.is_pseudo_element()) {
                        break;
                    }
                }
                // Every single selector in the list must be constrained.
                all_constrained &= nested.compound_state.constrained;
            }

            if offset >= s.len() {
                continue;
            }

            // Then, recurse in to check at the deeper level.
            if Self::has_warning(s, offset, self.compound_state.in_relative_selector) {
                self.compound_state.constrained = false;
                if !self.compound_state.in_relative_selector {
                    self.compound_state.relative_selector_found = true;
                }
                return false;
            }
        }
        self.compound_state.constrained |= all_constrained;
        true
    }

    fn visit_relative_selector_list(&mut self, list: &[RelativeSelector<Self::Impl>]) -> bool {
        debug_assert!(
            !self.compound_state.in_relative_selector,
            "Nested relative selector"
        );
        self.compound_state.relative_selector_found = true;

        for rs in list {
            // If the inside is unconstrained, we are unconstrained no matter what.
            if Self::has_warning(&rs.selector, 0, true) {
                self.compound_state.constrained = false;
                return false;
            }
        }
        true
    }
}

struct SiblingCombinatorAfterScopeSelectorVisitor {
    right_combinator_is_sibling: bool,
    found: bool,
}

impl SiblingCombinatorAfterScopeSelectorVisitor {
    fn new(right_combinator_is_sibling: bool) -> Self {
        Self {
            right_combinator_is_sibling,
            found: false,
        }
    }
    fn has_warning(selector: &Selector<SelectorImpl>) -> bool {
        if !selector.has_scope_selector() {
            return false;
        }
        let visitor = SiblingCombinatorAfterScopeSelectorVisitor::new(false);
        visitor.find_never_matching_scope_selector(selector)
    }

    fn find_never_matching_scope_selector(mut self, selector: &Selector<SelectorImpl>) -> bool {
        selector.visit(&mut self);
        self.found
    }
}

impl SelectorVisitor for SiblingCombinatorAfterScopeSelectorVisitor {
    type Impl = SelectorImpl;

    fn visit_simple_selector(&mut self, c: &Component<Self::Impl>) -> bool {
        if !matches!(c, Component::Scope | Component::ImplicitScope) {
            return true;
        }
        // e.g. `:scope ~ .a` will never match.
        if self.right_combinator_is_sibling {
            self.found = true;
        }
        true
    }

    fn visit_selector_list(
        &mut self,
        _list_kind: SelectorListKind,
        list: &[Selector<Self::Impl>],
    ) -> bool {
        for s in list {
            let list_visitor = Self::new(self.right_combinator_is_sibling);
            self.found |= list_visitor.find_never_matching_scope_selector(s);
        }
        true
    }

    fn visit_complex_selector(&mut self, combinator_to_right: Option<Combinator>) -> bool {
        if let Some(c) = combinator_to_right {
            // Subject compounds' state is determined by the outer visitor. e.g: When there's `:is(.a .b) ~ .c`,
            // the inner visitor is assumed to be constructed with right_combinator_is_sibling == true.
            self.right_combinator_is_sibling = c.is_sibling();
        }
        true
    }

    // It's harder to discern if use of :scope <sibling-combinator> is invalid - at least for now, defer.
}
