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

//! Wrapper around Gecko's CSS error reporting mechanism.

#![allow(unsafe_code)]

use cssparser::{serialize_identifier, CowRcStr, ToCss};
use cssparser::{BasicParseErrorKind, ParseError, ParseErrorKind, SourceLocation, Token};
use selectors::parser::SelectorParseErrorKind;
use selectors::SelectorList;
use std::ffi::CStr;
use std::ptr;
use style::error_reporting::{ContextualParseError, ParseErrorReporter};
use style::gecko_bindings::bindings;
use style::gecko_bindings::structs::URLExtraData as RawUrlExtraData;
use style::gecko_bindings::structs::{nsIURI, Loader, StyleSheet as DomStyleSheet};
use style::selector_parser::SelectorImpl;
use style::stylesheets::UrlExtraData;
use style_traits::{StyleParseErrorKind, ValueParseErrorKind};

pub type ErrorKind<'i> = ParseErrorKind<'i, StyleParseErrorKind<'i>>;

/// An error reporter with all the data we need to report errors.
pub struct ErrorReporter {
    sheet: *const DomStyleSheet,
    loader: *const Loader,
    uri: *mut nsIURI,
}

impl ErrorReporter {
    /// Create a new instance of the Gecko error reporter, if error reporting is
    /// enabled.
    pub fn new(
        sheet: *mut DomStyleSheet,
        loader: *mut Loader,
        extra_data: *mut RawUrlExtraData,
    ) -> Option<Self> {
        if !Self::reporting_enabled(sheet, loader) {
            return None;
        }

        let uri = unsafe {
            extra_data
                .as_ref()
                .map(|d| d.mBaseURI.raw::<nsIURI>())
                .unwrap_or(ptr::null_mut())
        };

        Some(ErrorReporter { sheet, loader, uri })
    }
}

enum ErrorString<'a> {
    Snippet(CowRcStr<'a>),
    Ident(CowRcStr<'a>),
    UnexpectedToken(Token<'a>),
}

impl<'a> ErrorString<'a> {
    fn into_str(self) -> CowRcStr<'a> {
        match self {
            ErrorString::Snippet(s) => s,
            ErrorString::UnexpectedToken(t) => t.to_css_string().into(),
            ErrorString::Ident(i) => {
                let mut s = String::new();
                serialize_identifier(&i, &mut s).unwrap();
                s.into()
            },
        }
    }
}

#[derive(Debug)]
enum Action {
    Nothing,
    Skip,
    Drop,
}

trait ErrorHelpers<'a> {
    fn error_data(self) -> (CowRcStr<'a>, ErrorKind<'a>);
    fn error_params(self) -> ErrorParams<'a>;
    fn selector_list(&self) -> Option<&'a SelectorList<SelectorImpl>>;
    fn to_gecko_message(&self) -> (Option<&'static CStr>, &'static CStr, Action);
}

fn extract_error_param<'a>(err: ErrorKind<'a>) -> Option<ErrorString<'a>> {
    Some(match err {
        ParseErrorKind::Basic(BasicParseErrorKind::UnexpectedToken(t)) => {
            ErrorString::UnexpectedToken(t)
        },

        ParseErrorKind::Basic(BasicParseErrorKind::AtRuleInvalid(i)) |
        ParseErrorKind::Custom(StyleParseErrorKind::UnsupportedAtRule(i)) => {
            let mut s = String::from("@");
            serialize_identifier(&i, &mut s).unwrap();
            ErrorString::Snippet(s.into())
        },

        ParseErrorKind::Custom(StyleParseErrorKind::OtherInvalidValue(property)) => {
            ErrorString::Snippet(property)
        },

        ParseErrorKind::Custom(StyleParseErrorKind::SelectorError(
            SelectorParseErrorKind::UnexpectedIdent(ident),
        )) => ErrorString::Ident(ident),

        ParseErrorKind::Custom(StyleParseErrorKind::UnknownProperty(property)) => {
            ErrorString::Ident(property)
        },

        ParseErrorKind::Custom(StyleParseErrorKind::UnexpectedTokenWithinNamespace(token)) => {
            ErrorString::UnexpectedToken(token)
        },

        _ => return None,
    })
}

struct ErrorParams<'a> {
    prefix_param: Option<ErrorString<'a>>,
    main_param: Option<ErrorString<'a>>,
}

/// If an error parameter is present in the given error, return it. Additionally return
/// a second parameter if it exists, for use in the prefix for the eventual error message.
fn extract_error_params<'a>(err: ErrorKind<'a>) -> Option<ErrorParams<'a>> {
    let (main, prefix) = match err {
        ParseErrorKind::Custom(StyleParseErrorKind::InvalidColor(property, token)) |
        ParseErrorKind::Custom(StyleParseErrorKind::InvalidFilter(property, token)) => (
            Some(ErrorString::Snippet(property.into())),
            Some(ErrorString::UnexpectedToken(token)),
        ),

        ParseErrorKind::Custom(StyleParseErrorKind::MediaQueryExpectedFeatureName(ident)) => {
            (Some(ErrorString::Ident(ident)), None)
        },

        ParseErrorKind::Basic(BasicParseErrorKind::UnexpectedToken(token)) |
        ParseErrorKind::Custom(StyleParseErrorKind::ValueError(
            ValueParseErrorKind::InvalidColor(token),
        )) => (Some(ErrorString::UnexpectedToken(token)), None),

        ParseErrorKind::Custom(StyleParseErrorKind::SelectorError(err)) => match err {
            SelectorParseErrorKind::UnexpectedTokenInAttributeSelector(t) |
            SelectorParseErrorKind::BadValueInAttr(t) |
            SelectorParseErrorKind::ExpectedBarInAttr(t) |
            SelectorParseErrorKind::NoQualifiedNameInAttributeSelector(t) |
            SelectorParseErrorKind::InvalidQualNameInAttr(t) |
            SelectorParseErrorKind::ExplicitNamespaceUnexpectedToken(t) |
            SelectorParseErrorKind::PseudoElementExpectedIdent(t) |
            SelectorParseErrorKind::NoIdentForPseudo(t) |
            SelectorParseErrorKind::ClassNeedsIdent(t) |
            SelectorParseErrorKind::PseudoElementExpectedColon(t) => {
                (None, Some(ErrorString::UnexpectedToken(t)))
            },
            SelectorParseErrorKind::ExpectedNamespace(namespace) => {
                (None, Some(ErrorString::Ident(namespace)))
            },
            SelectorParseErrorKind::UnsupportedPseudoClassOrElement(p) => {
                (None, Some(ErrorString::Ident(p)))
            },
            SelectorParseErrorKind::EmptySelector | SelectorParseErrorKind::DanglingCombinator => {
                (None, None)
            },
            SelectorParseErrorKind::EmptyNegation => (None, Some(ErrorString::Snippet(")".into()))),
            err => match extract_error_param(ParseErrorKind::Custom(
                StyleParseErrorKind::SelectorError(err),
            )) {
                Some(e) => (Some(e), None),
                None => return None,
            },
        },
        err => match extract_error_param(err) {
            Some(e) => (Some(e), None),
            None => return None,
        },
    };
    Some(ErrorParams {
        main_param: main,
        prefix_param: prefix,
    })
}

impl<'a> ErrorHelpers<'a> for ContextualParseError<'a> {
    fn error_data(self) -> (CowRcStr<'a>, ErrorKind<'a>) {
        match self {
            ContextualParseError::UnsupportedPropertyDeclaration(s, err, _) |
            ContextualParseError::UnsupportedFontFaceDescriptor(s, err) |
            ContextualParseError::UnsupportedFontFeatureValuesDescriptor(s, err) |
            ContextualParseError::InvalidKeyframeRule(s, err) |
            ContextualParseError::InvalidFontFeatureValuesRule(s, err) |
            ContextualParseError::UnsupportedKeyframePropertyDeclaration(s, err) |
            ContextualParseError::InvalidRule(s, err) |
            ContextualParseError::UnsupportedRule(s, err) |
            ContextualParseError::UnsupportedViewportDescriptorDeclaration(s, err) |
            ContextualParseError::UnsupportedCounterStyleDescriptorDeclaration(s, err) |
            ContextualParseError::InvalidMediaRule(s, err) |
            ContextualParseError::UnsupportedValue(s, err) => (s.into(), err.kind),
            ContextualParseError::InvalidCounterStyleWithoutSymbols(s) |
            ContextualParseError::InvalidCounterStyleNotEnoughSymbols(s) => (
                s.into(),
                ParseErrorKind::Custom(StyleParseErrorKind::UnspecifiedError.into()),
            ),
            ContextualParseError::InvalidCounterStyleWithoutAdditiveSymbols |
            ContextualParseError::InvalidCounterStyleExtendsWithSymbols |
            ContextualParseError::InvalidCounterStyleExtendsWithAdditiveSymbols => (
                "".into(),
                ParseErrorKind::Custom(StyleParseErrorKind::UnspecifiedError.into()),
            ),
        }
    }

    fn error_params(self) -> ErrorParams<'a> {
        let (s, error) = self.error_data();
        extract_error_params(error).unwrap_or_else(|| ErrorParams {
            main_param: Some(ErrorString::Snippet(s)),
            prefix_param: None,
        })
    }

    fn selector_list(&self) -> Option<&'a SelectorList<SelectorImpl>> {
        match *self {
            ContextualParseError::UnsupportedPropertyDeclaration(_, _, selectors) => selectors,
            _ => None,
        }
    }

    fn to_gecko_message(&self) -> (Option<&'static CStr>, &'static CStr, Action) {
        let (msg, action): (&CStr, Action) = match *self {
            ContextualParseError::UnsupportedPropertyDeclaration(
                _,
                ParseError {
                    kind: ParseErrorKind::Basic(BasicParseErrorKind::UnexpectedToken(_)),
                    ..
                },
                _,
            ) |
            ContextualParseError::UnsupportedPropertyDeclaration(
                _,
                ParseError {
                    kind: ParseErrorKind::Basic(BasicParseErrorKind::AtRuleInvalid(_)),
                    ..
                },
                _,
            ) => (cstr!("PEParseDeclarationDeclExpected"), Action::Skip),
            ContextualParseError::UnsupportedPropertyDeclaration(
                _,
                ParseError {
                    kind: ParseErrorKind::Custom(ref err),
                    ..
                },
                _,
            ) => match *err {
                StyleParseErrorKind::InvalidColor(_, _) => {
                    return (
                        Some(cstr!("PEColorNotColor")),
                        cstr!("PEValueParsingError"),
                        Action::Drop,
                    );
                },
                StyleParseErrorKind::InvalidFilter(_, _) => {
                    return (
                        Some(cstr!("PEExpectedNoneOrURLOrFilterFunction")),
                        cstr!("PEValueParsingError"),
                        Action::Drop,
                    );
                },
                StyleParseErrorKind::OtherInvalidValue(_) => {
                    (cstr!("PEValueParsingError"), Action::Drop)
                },
                _ => (cstr!("PEUnknownProperty"), Action::Drop),
            },
            ContextualParseError::UnsupportedPropertyDeclaration(..) => {
                (cstr!("PEUnknownProperty"), Action::Drop)
            },
            ContextualParseError::UnsupportedFontFaceDescriptor(..) => {
                (cstr!("PEUnknownFontDesc"), Action::Skip)
            },
            ContextualParseError::InvalidKeyframeRule(..) => {
                (cstr!("PEKeyframeBadName"), Action::Nothing)
            },
            ContextualParseError::UnsupportedKeyframePropertyDeclaration(..) => {
                (cstr!("PEBadSelectorKeyframeRuleIgnored"), Action::Nothing)
            },
            ContextualParseError::InvalidRule(
                _,
                ParseError {
                    kind:
                        ParseErrorKind::Custom(StyleParseErrorKind::UnexpectedTokenWithinNamespace(_)),
                    ..
                },
            ) => (cstr!("PEAtNSUnexpected"), Action::Nothing),
            ContextualParseError::InvalidRule(
                _,
                ParseError {
                    kind: ParseErrorKind::Basic(BasicParseErrorKind::AtRuleInvalid(_)),
                    ..
                },
            ) |
            ContextualParseError::InvalidRule(
                _,
                ParseError {
                    kind: ParseErrorKind::Custom(StyleParseErrorKind::UnsupportedAtRule(_)),
                    ..
                },
            ) => (cstr!("PEUnknownAtRule"), Action::Nothing),
            ContextualParseError::InvalidRule(_, ref err) => {
                let prefix = match err.kind {
                    ParseErrorKind::Custom(StyleParseErrorKind::SelectorError(ref err)) => {
                        match *err {
                            SelectorParseErrorKind::UnexpectedTokenInAttributeSelector(_) => {
                                Some(cstr!("PEAttSelUnexpected"))
                            },
                            SelectorParseErrorKind::ExpectedBarInAttr(_) => {
                                Some(cstr!("PEAttSelNoBar"))
                            },
                            SelectorParseErrorKind::BadValueInAttr(_) => {
                                Some(cstr!("PEAttSelBadValue"))
                            },
                            SelectorParseErrorKind::NoQualifiedNameInAttributeSelector(_) => {
                                Some(cstr!("PEAttributeNameOrNamespaceExpected"))
                            },
                            SelectorParseErrorKind::InvalidQualNameInAttr(_) => {
                                Some(cstr!("PEAttributeNameExpected"))
                            },
                            SelectorParseErrorKind::ExplicitNamespaceUnexpectedToken(_) => {
                                Some(cstr!("PETypeSelNotType"))
                            },
                            SelectorParseErrorKind::ExpectedNamespace(_) => {
                                Some(cstr!("PEUnknownNamespacePrefix"))
                            },
                            SelectorParseErrorKind::EmptySelector => {
                                Some(cstr!("PESelectorGroupNoSelector"))
                            },
                            SelectorParseErrorKind::DanglingCombinator => {
                                Some(cstr!("PESelectorGroupExtraCombinator"))
                            },
                            SelectorParseErrorKind::UnsupportedPseudoClassOrElement(_) => {
                                Some(cstr!("PEPseudoSelUnknown"))
                            },
                            SelectorParseErrorKind::PseudoElementExpectedColon(_) => {
                                Some(cstr!("PEPseudoSelEndOrUserActionPC"))
                            },
                            SelectorParseErrorKind::NoIdentForPseudo(_) => {
                                Some(cstr!("PEPseudoClassArgNotIdent"))
                            },
                            SelectorParseErrorKind::PseudoElementExpectedIdent(_) => {
                                Some(cstr!("PEPseudoSelBadName"))
                            },
                            SelectorParseErrorKind::ClassNeedsIdent(_) => {
                                Some(cstr!("PEClassSelNotIdent"))
                            },
                            SelectorParseErrorKind::EmptyNegation => {
                                Some(cstr!("PENegationBadArg"))
                            },
                            _ => None,
                        }
                    },
                    _ => None,
                };
                return (prefix, cstr!("PEBadSelectorRSIgnored"), Action::Nothing);
            },
            ContextualParseError::InvalidMediaRule(_, ref err) => {
                let err: &CStr = match err.kind {
                    ParseErrorKind::Custom(StyleParseErrorKind::MediaQueryExpectedFeatureName(
                        ..
                    )) => cstr!("PEMQExpectedFeatureName"),
                    ParseErrorKind::Custom(StyleParseErrorKind::MediaQueryExpectedFeatureValue) => {
                        cstr!("PEMQExpectedFeatureValue")
                    },
                    ParseErrorKind::Custom(StyleParseErrorKind::MediaQueryUnexpectedOperator) => {
                        cstr!("PEMQUnexpectedOperator")
                    },
                    ParseErrorKind::Custom(StyleParseErrorKind::RangedExpressionWithNoValue) => {
                        cstr!("PEMQNoMinMaxWithoutValue")
                    },
                    _ => cstr!("PEMQUnexpectedToken"),
                };
                (err, Action::Nothing)
            },
            ContextualParseError::UnsupportedRule(..) => (cstr!("PEDeclDropped"), Action::Nothing),
            ContextualParseError::UnsupportedViewportDescriptorDeclaration(..) |
            ContextualParseError::UnsupportedCounterStyleDescriptorDeclaration(..) |
            ContextualParseError::InvalidCounterStyleWithoutSymbols(..) |
            ContextualParseError::InvalidCounterStyleNotEnoughSymbols(..) |
            ContextualParseError::InvalidCounterStyleWithoutAdditiveSymbols |
            ContextualParseError::InvalidCounterStyleExtendsWithSymbols |
            ContextualParseError::InvalidCounterStyleExtendsWithAdditiveSymbols |
            ContextualParseError::UnsupportedFontFeatureValuesDescriptor(..) |
            ContextualParseError::InvalidFontFeatureValuesRule(..) => {
                (cstr!("PEUnknownAtRule"), Action::Skip)
            },
            ContextualParseError::UnsupportedValue(_, ParseError { ref kind, .. }) => {
                match *kind {
                    ParseErrorKind::Custom(StyleParseErrorKind::ValueError(
                        ValueParseErrorKind::InvalidColor(..),
                    )) => (cstr!("PEColorNotColor"), Action::Nothing),
                    _ => {
                        // Not the best error message, since we weren't parsing
                        // a declaration, just a value. But we don't produce
                        // UnsupportedValue errors other than InvalidColors
                        // currently.
                        debug_assert!(false, "should use a more specific error message");
                        (cstr!("PEDeclDropped"), Action::Nothing)
                    },
                }
            },
        };
        (None, msg, action)
    }
}

impl ErrorReporter {
    fn reporting_enabled(sheet: *const DomStyleSheet, loader: *const Loader) -> bool {
        unsafe { bindings::Gecko_ErrorReportingEnabled(sheet, loader) }
    }

    pub fn report(&self, location: SourceLocation, error: ContextualParseError) {
        debug_assert!(Self::reporting_enabled(self.sheet, self.loader));

        let (pre, name, action) = error.to_gecko_message();
        let suffix = match action {
            Action::Nothing => ptr::null(),
            Action::Skip => cstr!("PEDeclSkipped").as_ptr(),
            Action::Drop => cstr!("PEDeclDropped").as_ptr(),
        };
        let selector_list = error.selector_list().map(|l| l.to_css_string());
        let selector_list_ptr = selector_list.as_ref().map_or(ptr::null(), |s| s.as_ptr()) as *const _;
        let params = error.error_params();
        let param = params.main_param;
        let pre_param = params.prefix_param;
        let param = param.map(|p| p.into_str());
        let pre_param = pre_param.map(|p| p.into_str());
        let param_ptr = param.as_ref().map_or(ptr::null(), |p| p.as_ptr());
        let pre_param_ptr = pre_param.as_ref().map_or(ptr::null(), |p| p.as_ptr());
        // The CSS source text is unused and will be removed in bug 1381188.
        let source = "";
        unsafe {
            bindings::Gecko_ReportUnexpectedCSSError(
                self.sheet,
                self.loader,
                self.uri,
                name.as_ptr() as *const _,
                param_ptr as *const _,
                param.as_ref().map_or(0, |p| p.len()) as u32,
                pre.map_or(ptr::null(), |p| p.as_ptr()) as *const _,
                pre_param_ptr as *const _,
                pre_param.as_ref().map_or(0, |p| p.len()) as u32,
                suffix as *const _,
                source.as_ptr() as *const _,
                source.len() as u32,
                selector_list_ptr,
                selector_list.as_ref().map_or(0, |string| string.len()) as u32,
                location.line,
                location.column,
            );
        }
    }
}

impl ParseErrorReporter for ErrorReporter {
    fn report_error(
        &self,
        _url: &UrlExtraData,
        location: SourceLocation,
        error: ContextualParseError,
    ) {
        self.report(location, error)
    }
}
