/* 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 http://mozilla.org/MPL/2.0/. */

//! Parsing of the stylesheet contents.

use {Namespace, Prefix};
use counter_style::{parse_counter_style_body, parse_counter_style_name};
use cssparser::{AtRuleParser, AtRuleType, Parser, QualifiedRuleParser, RuleListParser, SourceLocation};
use error_reporting::ContextualParseError;
use font_face::parse_font_face_block;
use media_queries::{parse_media_query_list, MediaList};
use parking_lot::RwLock;
use parser::{Parse, ParserContext, log_css_error};
use properties::parse_property_declaration_list;
use selector_parser::{SelectorImpl, SelectorParser};
use selectors::SelectorList;
use selectors::parser::SelectorParseError;
use shared_lock::{Locked, SharedRwLock};
use std::borrow::Cow;
use std::sync::atomic::AtomicBool;
use str::starts_with_ignore_ascii_case;
use style_traits::{StyleParseError, ParseError};
use stylearc::Arc;
use stylesheets::{CssRule, CssRules, CssRuleType, Origin, StylesheetLoader};
use stylesheets::{DocumentRule, ImportRule, KeyframesRule, MediaRule, NamespaceRule, PageRule};
use stylesheets::{StyleRule, SupportsRule, ViewportRule};
use stylesheets::document_rule::DocumentCondition;
use stylesheets::keyframes_rule::parse_keyframe_list;
use stylesheets::loader::NoOpLoader;
use stylesheets::stylesheet::{Namespaces, Stylesheet};
use stylesheets::supports_rule::SupportsCondition;
use values::CustomIdent;
use values::KeyframesName;
use values::specified::url::SpecifiedUrl;

/// The parser for the top-level rules in a stylesheet.
pub struct TopLevelRuleParser<'a> {
    /// The origin of the stylesheet we're parsing.
    pub stylesheet_origin: Origin,
    /// A reference to the lock we need to use to create rules.
    pub shared_lock: &'a SharedRwLock,
    /// A reference to a stylesheet loader if applicable, for `@import` rules.
    pub loader: Option<&'a StylesheetLoader>,
    /// The parser context. This initially won't contain any namespaces, but
    /// will be populated after parsing namespace rules, if any.
    pub context: ParserContext<'a>,
    /// The current state of the parser.
    pub state: State,
    /// The namespace map we use for parsing. Needs to start as `Some()`, and
    /// will be taken out after parsing namespace rules, and that reference will
    /// be moved to `ParserContext`.
    pub namespaces: Option<&'a mut Namespaces>,
}

impl<'b> TopLevelRuleParser<'b> {
    fn nested<'a: 'b>(&'a self) -> NestedRuleParser<'a, 'b> {
        NestedRuleParser {
            stylesheet_origin: self.stylesheet_origin,
            shared_lock: self.shared_lock,
            context: &self.context,
        }
    }

    /// Returns the associated parser context with this rule parser.
    pub fn context(&self) -> &ParserContext {
        &self.context
    }

    /// Returns the current state of the parser.
    pub fn state(&self) -> State {
        self.state
    }
}

/// The current state of the parser.
#[derive(Eq, PartialEq, Ord, PartialOrd, Copy, Clone)]
pub enum State {
    /// We haven't started parsing rules.
    Start = 1,
    /// We're parsing `@import` rules.
    Imports = 2,
    /// We're parsing `@namespace` rules.
    Namespaces = 3,
    /// We're parsing the main body of the stylesheet.
    Body = 4,
    /// We've found an invalid state (as, a namespace rule after style rules),
    /// and the rest of the stylesheet should be ignored.
    Invalid = 5,
}

#[derive(Clone, Debug)]
#[cfg_attr(feature = "servo", derive(HeapSizeOf))]
/// Vendor prefix.
pub enum VendorPrefix {
    /// -moz prefix.
    Moz,
    /// -webkit prefix.
    WebKit,
}

/// A rule prelude for a given at-rule.
pub enum AtRulePrelude {
    /// A @font-face rule prelude.
    FontFace(SourceLocation),
    /// A @counter-style rule prelude, with its counter style name.
    CounterStyle(CustomIdent),
    /// A @media rule prelude, with its media queries.
    Media(Arc<Locked<MediaList>>, SourceLocation),
    /// An @supports rule, with its conditional
    Supports(SupportsCondition, SourceLocation),
    /// A @viewport rule prelude.
    Viewport,
    /// A @keyframes rule, with its animation name and vendor prefix if exists.
    Keyframes(KeyframesName, Option<VendorPrefix>, SourceLocation),
    /// A @page rule prelude.
    Page(SourceLocation),
    /// A @document rule, with its conditional.
    Document(DocumentCondition, SourceLocation),
}


#[cfg(feature = "gecko")]
fn register_namespace(ns: &Namespace) -> Result<i32, ()> {
    use gecko_bindings::bindings;
    let id = unsafe { bindings::Gecko_RegisterNamespace(ns.0.as_ptr()) };
    if id == -1 {
        Err(())
    } else {
        Ok(id)
    }
}

#[cfg(feature = "servo")]
fn register_namespace(_: &Namespace) -> Result<(), ()> {
    Ok(()) // servo doesn't use namespace ids
}

impl<'a, 'i> AtRuleParser<'i> for TopLevelRuleParser<'a> {
    type Prelude = AtRulePrelude;
    type AtRule = CssRule;
    type Error = SelectorParseError<'i, StyleParseError<'i>>;

    fn parse_prelude<'t>(
        &mut self,
        name: Cow<'i, str>,
        input: &mut Parser<'i, 't>
    ) -> Result<AtRuleType<AtRulePrelude, CssRule>, ParseError<'i>> {
        let location = get_location_with_offset(input.current_source_location(),
                                                self.context.line_number_offset);
        match_ignore_ascii_case! { &*name,
            "import" => {
                if self.state > State::Imports {
                    self.state = State::Invalid;
                    // "@import must be before any rule but @charset"
                    return Err(StyleParseError::UnexpectedImportRule.into())
                }

                self.state = State::Imports;
                let url_string = input.expect_url_or_string()?;
                let specified_url = SpecifiedUrl::parse_from_string(url_string, &self.context)?;

                let media = parse_media_query_list(&self.context, input);
                let media = Arc::new(self.shared_lock.wrap(media));

                let noop_loader = NoOpLoader;
                let loader = if !specified_url.is_invalid() {
                    self.loader.expect("Expected a stylesheet loader for @import")
                } else {
                    &noop_loader
                };

                let mut specified_url = Some(specified_url);
                let arc = loader.request_stylesheet(media, &mut |media| {
                    ImportRule {
                        url: specified_url.take().unwrap(),
                        stylesheet: Arc::new(Stylesheet {
                            rules: CssRules::new(Vec::new(), self.shared_lock),
                            media: media,
                            shared_lock: self.shared_lock.clone(),
                            origin: self.context.stylesheet_origin,
                            url_data: self.context.url_data.clone(),
                            namespaces: RwLock::new(Namespaces::default()),
                            dirty_on_viewport_size_change: AtomicBool::new(false),
                            disabled: AtomicBool::new(false),
                            quirks_mode: self.context.quirks_mode,
                        }),
                        source_location: location,
                    }
                }, &mut |import_rule| {
                    Arc::new(self.shared_lock.wrap(import_rule))
                });

                return Ok(AtRuleType::WithoutBlock(CssRule::Import(arc)))
            },
            "namespace" => {
                if self.state > State::Namespaces {
                    self.state = State::Invalid;
                    // "@namespace must be before any rule but @charset and @import"
                    return Err(StyleParseError::UnexpectedNamespaceRule.into())
                }
                self.state = State::Namespaces;

                let prefix_result = input.try(|input| input.expect_ident());
                let url = Namespace::from(try!(input.expect_url_or_string()));

                let id = register_namespace(&url)
                    .map_err(|()| StyleParseError::UnspecifiedError)?;

                let mut namespaces = self.namespaces.as_mut().unwrap();

                let opt_prefix = if let Ok(prefix) = prefix_result {
                    let prefix = Prefix::from(prefix);
                    namespaces
                        .prefixes
                        .insert(prefix.clone(), (url.clone(), id));
                    Some(prefix)
                } else {
                    namespaces.default = Some((url.clone(), id));
                    None
                };

                return Ok(AtRuleType::WithoutBlock(CssRule::Namespace(Arc::new(
                    self.shared_lock.wrap(NamespaceRule {
                        prefix: opt_prefix,
                        url: url,
                        source_location: location,
                    })
                ))))
            },
            // @charset is removed by rust-cssparser if it’s the first rule in the stylesheet
            // anything left is invalid.
            "charset" => return Err(StyleParseError::UnexpectedCharsetRule.into()),
            _ => {}
        }
        // Don't allow starting with an invalid state
        if self.state > State::Body {
            self.state = State::Invalid;
            return Err(StyleParseError::UnspecifiedError.into());
        }
        self.state = State::Body;

        // "Freeze" the namespace map (no more namespace rules can be parsed
        // after this point), and stick it in the context.
        if self.namespaces.is_some() {
            let namespaces = &*self.namespaces.take().unwrap();
            self.context.namespaces = Some(namespaces);
        }
        AtRuleParser::parse_prelude(&mut self.nested(), name, input)
    }

    #[inline]
    fn parse_block<'t>(&mut self, prelude: AtRulePrelude, input: &mut Parser<'i, 't>)
                       -> Result<CssRule, ParseError<'i>> {
        AtRuleParser::parse_block(&mut self.nested(), prelude, input)
    }
}


impl<'a, 'i> QualifiedRuleParser<'i> for TopLevelRuleParser<'a> {
    type Prelude = SelectorList<SelectorImpl>;
    type QualifiedRule = CssRule;
    type Error = SelectorParseError<'i, StyleParseError<'i>>;

    #[inline]
    fn parse_prelude<'t>(&mut self, input: &mut Parser<'i, 't>)
                         -> Result<SelectorList<SelectorImpl>, ParseError<'i>> {
        self.state = State::Body;

        // "Freeze" the namespace map (no more namespace rules can be parsed
        // after this point), and stick it in the context.
        if self.namespaces.is_some() {
            let namespaces = &*self.namespaces.take().unwrap();
            self.context.namespaces = Some(namespaces);
        }

        QualifiedRuleParser::parse_prelude(&mut self.nested(), input)
    }

    #[inline]
    fn parse_block<'t>(
        &mut self,
        prelude: SelectorList<SelectorImpl>,
        input: &mut Parser<'i, 't>
    ) -> Result<CssRule, ParseError<'i>> {
        QualifiedRuleParser::parse_block(&mut self.nested(), prelude, input)
    }
}

#[derive(Clone)]  // shallow, relatively cheap .clone
struct NestedRuleParser<'a, 'b: 'a> {
    stylesheet_origin: Origin,
    shared_lock: &'a SharedRwLock,
    context: &'a ParserContext<'b>,
}

impl<'a, 'b> NestedRuleParser<'a, 'b> {
    fn parse_nested_rules(
        &mut self,
        input: &mut Parser,
        rule_type: CssRuleType
    ) -> Arc<Locked<CssRules>> {
        let context = ParserContext::new_with_rule_type(self.context, Some(rule_type));

        let nested_parser = NestedRuleParser {
            stylesheet_origin: self.stylesheet_origin,
            shared_lock: self.shared_lock,
            context: &context,
        };

        let mut iter = RuleListParser::new_for_nested_rule(input, nested_parser);
        let mut rules = Vec::new();
        while let Some(result) = iter.next() {
            match result {
                Ok(rule) => rules.push(rule),
                Err(err) => {
                    let pos = err.span.start;
                    let error = ContextualParseError::UnsupportedRule(
                        iter.input.slice(err.span), err.error);
                    log_css_error(iter.input, pos, error, self.context);
                }
            }
        }
        CssRules::new(rules, self.shared_lock)
    }
}

#[cfg(feature = "servo")]
fn is_viewport_enabled() -> bool {
    use servo_config::prefs::PREFS;
    PREFS.get("layout.viewport.enabled").as_boolean().unwrap_or(false)
}

#[cfg(not(feature = "servo"))]
fn is_viewport_enabled() -> bool {
    false // Gecko doesn't support @viewport.
}

impl<'a, 'b, 'i> AtRuleParser<'i> for NestedRuleParser<'a, 'b> {
    type Prelude = AtRulePrelude;
    type AtRule = CssRule;
    type Error = SelectorParseError<'i, StyleParseError<'i>>;

    fn parse_prelude<'t>(
        &mut self,
        name: Cow<'i, str>,
        input: &mut Parser<'i, 't>
    ) -> Result<AtRuleType<AtRulePrelude, CssRule>, ParseError<'i>> {
        let location =
            get_location_with_offset(
                input.current_source_location(),
                self.context.line_number_offset
            );

        match_ignore_ascii_case! { &*name,
            "media" => {
                let media_queries = parse_media_query_list(self.context, input);
                let arc = Arc::new(self.shared_lock.wrap(media_queries));
                Ok(AtRuleType::WithBlock(AtRulePrelude::Media(arc, location)))
            },
            "supports" => {
                let cond = SupportsCondition::parse(input)?;
                Ok(AtRuleType::WithBlock(AtRulePrelude::Supports(cond, location)))
            },
            "font-face" => {
                Ok(AtRuleType::WithBlock(AtRulePrelude::FontFace(location)))
            },
            "counter-style" => {
                if !cfg!(feature = "gecko") {
                    // Support for this rule is not fully implemented in Servo yet.
                    return Err(StyleParseError::UnsupportedAtRule(name.clone()).into())
                }
                let name = parse_counter_style_name(input)?;
                // ASCII-case-insensitive matches for "decimal" are already
                // lower-cased by `parse_counter_style_name`, so we can use ==
                // here.
                if name.0 == atom!("decimal") {
                    return Err(StyleParseError::UnspecifiedError.into())
                }
                Ok(AtRuleType::WithBlock(AtRulePrelude::CounterStyle(name)))
            },
            "viewport" => {
                if is_viewport_enabled() {
                    Ok(AtRuleType::WithBlock(AtRulePrelude::Viewport))
                } else {
                    Err(StyleParseError::UnsupportedAtRule(name.clone()).into())
                }
            },
            "keyframes" | "-webkit-keyframes" | "-moz-keyframes" => {
                let prefix = if starts_with_ignore_ascii_case(&*name, "-webkit-") {
                    Some(VendorPrefix::WebKit)
                } else if starts_with_ignore_ascii_case(&*name, "-moz-") {
                    Some(VendorPrefix::Moz)
                } else {
                    None
                };
                if cfg!(feature = "servo") &&
                   prefix.as_ref().map_or(false, |p| matches!(*p, VendorPrefix::Moz)) {
                    // Servo should not support @-moz-keyframes.
                    return Err(StyleParseError::UnsupportedAtRule(name.clone()).into())
                }
                let name = KeyframesName::parse(self.context, input)?;

                Ok(AtRuleType::WithBlock(AtRulePrelude::Keyframes(name, prefix, location)))
            },
            "page" => {
                if cfg!(feature = "gecko") {
                    Ok(AtRuleType::WithBlock(AtRulePrelude::Page(location)))
                } else {
                    Err(StyleParseError::UnsupportedAtRule(name.clone()).into())
                }
            },
            "-moz-document" => {
                if cfg!(feature = "gecko") {
                    let cond = DocumentCondition::parse(self.context, input)?;
                    Ok(AtRuleType::WithBlock(AtRulePrelude::Document(cond, location)))
                } else {
                    Err(StyleParseError::UnsupportedAtRule(name.clone()).into())
                }
            },
            _ => Err(StyleParseError::UnsupportedAtRule(name.clone()).into())
        }
    }

    fn parse_block<'t>(
        &mut self,
        prelude: AtRulePrelude,
        input: &mut Parser<'i, 't>
    ) -> Result<CssRule, ParseError<'i>> {
        match prelude {
            AtRulePrelude::FontFace(location) => {
                let context = ParserContext::new_with_rule_type(self.context, Some(CssRuleType::FontFace));
                Ok(CssRule::FontFace(Arc::new(self.shared_lock.wrap(
                   parse_font_face_block(&context, input, location).into()))))
            }
            AtRulePrelude::CounterStyle(name) => {
                let context = ParserContext::new_with_rule_type(self.context, Some(CssRuleType::CounterStyle));
                Ok(CssRule::CounterStyle(Arc::new(self.shared_lock.wrap(
                   parse_counter_style_body(name, &context, input)?.into()))))
            }
            AtRulePrelude::Media(media_queries, location) => {
                Ok(CssRule::Media(Arc::new(self.shared_lock.wrap(MediaRule {
                    media_queries: media_queries,
                    rules: self.parse_nested_rules(input, CssRuleType::Media),
                    source_location: location,
                }))))
            }
            AtRulePrelude::Supports(cond, location) => {
                let enabled = cond.eval(self.context);
                Ok(CssRule::Supports(Arc::new(self.shared_lock.wrap(SupportsRule {
                    condition: cond,
                    rules: self.parse_nested_rules(input, CssRuleType::Supports),
                    enabled: enabled,
                    source_location: location,
                }))))
            }
            AtRulePrelude::Viewport => {
                let context = ParserContext::new_with_rule_type(self.context, Some(CssRuleType::Viewport));
                Ok(CssRule::Viewport(Arc::new(self.shared_lock.wrap(
                   try!(ViewportRule::parse(&context, input))))))
            }
            AtRulePrelude::Keyframes(name, prefix, location) => {
                let context = ParserContext::new_with_rule_type(self.context, Some(CssRuleType::Keyframes));
                Ok(CssRule::Keyframes(Arc::new(self.shared_lock.wrap(KeyframesRule {
                    name: name,
                    keyframes: parse_keyframe_list(&context, input, self.shared_lock),
                    vendor_prefix: prefix,
                    source_location: location,
                }))))
            }
            AtRulePrelude::Page(location) => {
                let context = ParserContext::new_with_rule_type(self.context, Some(CssRuleType::Page));
                let declarations = parse_property_declaration_list(&context, input);
                Ok(CssRule::Page(Arc::new(self.shared_lock.wrap(PageRule {
                    block: Arc::new(self.shared_lock.wrap(declarations)),
                    source_location: location,
                }))))
            }
            AtRulePrelude::Document(cond, location) => {
                if cfg!(feature = "gecko") {
                    Ok(CssRule::Document(Arc::new(self.shared_lock.wrap(DocumentRule {
                        condition: cond,
                        rules: self.parse_nested_rules(input, CssRuleType::Document),
                        source_location: location,
                    }))))
                } else {
                    unreachable!()
                }
            }
        }
    }
}

impl<'a, 'b, 'i> QualifiedRuleParser<'i> for NestedRuleParser<'a, 'b> {
    type Prelude = SelectorList<SelectorImpl>;
    type QualifiedRule = CssRule;
    type Error = SelectorParseError<'i, StyleParseError<'i>>;

    fn parse_prelude<'t>(&mut self, input: &mut Parser<'i, 't>)
                         -> Result<SelectorList<SelectorImpl>, ParseError<'i>> {
        let selector_parser = SelectorParser {
            stylesheet_origin: self.stylesheet_origin,
            namespaces: self.context.namespaces.unwrap(),
        };

        SelectorList::parse(&selector_parser, input)
    }

    fn parse_block<'t>(
        &mut self,
        prelude: SelectorList<SelectorImpl>,
        input: &mut Parser<'i, 't>
    ) -> Result<CssRule, ParseError<'i>> {
        let location = get_location_with_offset(input.current_source_location(),
                                                self.context.line_number_offset);
        let context = ParserContext::new_with_rule_type(self.context, Some(CssRuleType::Style));
        let declarations = parse_property_declaration_list(&context, input);
        Ok(CssRule::Style(Arc::new(self.shared_lock.wrap(StyleRule {
            selectors: prelude,
            block: Arc::new(self.shared_lock.wrap(declarations)),
            source_location: location,
        }))))
    }
}

/// Calculates the location of a rule's source given an offset.
fn get_location_with_offset(
    location: SourceLocation,
    offset: u64
) -> SourceLocation {
    SourceLocation {
        line: location.line + offset as usize - 1,
        column: location.column,
    }
}
