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

//! [@document rules](https://www.w3.org/TR/2012/WD-css3-conditional-20120911/#at-document)
//! initially in CSS Conditional Rules Module Level 3, @document has been postponed to the level 4.
//! We implement the prefixed `@-moz-document`.

use crate::media_queries::Device;
use crate::parser::{Parse, ParserContext};
use crate::shared_lock::{DeepCloneWithLock, Locked};
use crate::shared_lock::{SharedRwLock, SharedRwLockReadGuard, ToCssWithGuard};
use crate::stylesheets::CssRules;
use crate::values::CssUrl;
use cssparser::{BasicParseErrorKind, Parser, SourceLocation};
#[cfg(feature = "gecko")]
use malloc_size_of::{MallocSizeOfOps, MallocUnconditionalShallowSizeOf};
use servo_arc::Arc;
use std::fmt::{self, Write};
use style_traits::{CssStringWriter, CssWriter, ParseError, StyleParseErrorKind, ToCss};

#[derive(Debug, ToShmem)]
/// A @-moz-document rule
pub struct DocumentRule {
    /// The parsed condition
    pub condition: DocumentCondition,
    /// Child rules
    pub rules: Arc<Locked<CssRules>>,
    /// The line and column of the rule's source code.
    pub source_location: SourceLocation,
}

impl DocumentRule {
    /// Measure heap usage.
    #[cfg(feature = "gecko")]
    pub fn size_of(&self, guard: &SharedRwLockReadGuard, ops: &mut MallocSizeOfOps) -> usize {
        // Measurement of other fields may be added later.
        self.rules.unconditional_shallow_size_of(ops)
            + self.rules.read_with(guard).size_of(guard, ops)
    }
}

impl ToCssWithGuard for DocumentRule {
    fn to_css(&self, guard: &SharedRwLockReadGuard, dest: &mut CssStringWriter) -> fmt::Result {
        dest.write_str("@-moz-document ")?;
        self.condition.to_css(&mut CssWriter::new(dest))?;
        dest.write_str(" {")?;
        for rule in self.rules.read_with(guard).0.iter() {
            dest.write_char(' ')?;
            rule.to_css(guard, dest)?;
        }
        dest.write_str(" }")
    }
}

impl DeepCloneWithLock for DocumentRule {
    /// Deep clones this DocumentRule.
    fn deep_clone_with_lock(&self, lock: &SharedRwLock, guard: &SharedRwLockReadGuard) -> Self {
        let rules = self.rules.read_with(guard);
        DocumentRule {
            condition: self.condition.clone(),
            rules: Arc::new(lock.wrap(rules.deep_clone_with_lock(lock, guard))),
            source_location: self.source_location.clone(),
        }
    }
}

/// The kind of media document that the rule will match.
#[derive(Clone, Copy, Debug, Parse, PartialEq, ToCss, ToShmem)]
#[allow(missing_docs)]
pub enum MediaDocumentKind {
    All,
    Image,
    Video,
}

/// A matching function for a `@document` rule's condition.
#[derive(Clone, Debug, ToCss, ToShmem)]
pub enum DocumentMatchingFunction {
    /// Exact URL matching function. It evaluates to true whenever the
    /// URL of the document being styled is exactly the URL given.
    Url(CssUrl),
    /// URL prefix matching function. It evaluates to true whenever the
    /// URL of the document being styled has the argument to the
    /// function as an initial substring (which is true when the two
    /// strings are equal). When the argument is the empty string,
    /// it evaluates to true for all documents.
    #[css(function)]
    UrlPrefix(String),
    /// Domain matching function. It evaluates to true whenever the URL
    /// of the document being styled has a host subcomponent and that
    /// host subcomponent is exactly the argument to the ‘domain()’
    /// function or a final substring of the host component is a
    /// period (U+002E) immediately followed by the argument to the
    /// ‘domain()’ function.
    #[css(function)]
    Domain(String),
    /// Regular expression matching function. It evaluates to true
    /// whenever the regular expression matches the entirety of the URL
    /// of the document being styled.
    #[css(function)]
    Regexp(String),
    /// Matching function for a media document.
    #[css(function)]
    MediaDocument(MediaDocumentKind),
    /// Matching function for a plain-text document.
    #[css(function)]
    PlainTextDocument(()),
    /// Matching function for a document that can be observed by other content
    /// documents.
    #[css(function)]
    UnobservableDocument(()),
}

macro_rules! parse_quoted_or_unquoted_string {
    ($input:ident, $url_matching_function:expr) => {
        $input.parse_nested_block(|input| {
            let start = input.position();
            input
                .parse_entirely(|input| {
                    let string = input.expect_string()?;
                    Ok($url_matching_function(string.as_ref().to_owned()))
                })
                .or_else(|_: ParseError| {
                    while let Ok(_) = input.next() {}
                    Ok($url_matching_function(input.slice_from(start).to_string()))
                })
        })
    };
}

impl DocumentMatchingFunction {
    /// Parse a URL matching function for a`@document` rule's condition.
    pub fn parse<'i, 't>(
        context: &ParserContext,
        input: &mut Parser<'i, 't>,
    ) -> Result<Self, ParseError<'i>> {
        if let Ok(url) = input.try_parse(|input| CssUrl::parse(context, input)) {
            return Ok(DocumentMatchingFunction::Url(url));
        }

        let location = input.current_source_location();
        let function = input.expect_function()?.clone();
        match_ignore_ascii_case! { &function,
            "url-prefix" => {
                parse_quoted_or_unquoted_string!(input, DocumentMatchingFunction::UrlPrefix)
            },
            "domain" => {
                parse_quoted_or_unquoted_string!(input, DocumentMatchingFunction::Domain)
            },
            "regexp" => {
                input.parse_nested_block(|input| {
                    Ok(DocumentMatchingFunction::Regexp(
                        input.expect_string()?.as_ref().to_owned(),
                    ))
                })
            },
            "media-document" => {
                input.parse_nested_block(|input| {
                    let kind = MediaDocumentKind::parse(input)?;
                    Ok(DocumentMatchingFunction::MediaDocument(kind))
                })
            },

            "plain-text-document" => {
                input.parse_nested_block(|input| {
                    input.expect_exhausted()?;
                    Ok(DocumentMatchingFunction::PlainTextDocument(()))
                })
            },

            "unobservable-document" => {
                input.parse_nested_block(|input| {
                    input.expect_exhausted()?;
                    Ok(DocumentMatchingFunction::UnobservableDocument(()))
                })
            },

            _ => {
                Err(location.new_custom_error(
                    StyleParseErrorKind::UnexpectedFunction(function.clone())
                ))
            },
        }
    }

    #[cfg(feature = "gecko")]
    /// Evaluate a URL matching function.
    pub fn evaluate(&self, device: &Device) -> bool {
        use crate::gecko_bindings::bindings::Gecko_DocumentRule_UseForPresentation;
        use crate::gecko_bindings::structs::DocumentMatchingFunction as GeckoDocumentMatchingFunction;
        use nsstring::nsCStr;

        let func = match *self {
            DocumentMatchingFunction::Url(_) => GeckoDocumentMatchingFunction::URL,
            DocumentMatchingFunction::UrlPrefix(_) => GeckoDocumentMatchingFunction::URLPrefix,
            DocumentMatchingFunction::Domain(_) => GeckoDocumentMatchingFunction::Domain,
            DocumentMatchingFunction::Regexp(_) => GeckoDocumentMatchingFunction::RegExp,
            DocumentMatchingFunction::MediaDocument(_) => {
                GeckoDocumentMatchingFunction::MediaDocument
            },
            DocumentMatchingFunction::PlainTextDocument(..) => {
                GeckoDocumentMatchingFunction::PlainTextDocument
            },
            DocumentMatchingFunction::UnobservableDocument(..) => {
                GeckoDocumentMatchingFunction::UnobservableDocument
            },
        };

        let pattern = nsCStr::from(match *self {
            DocumentMatchingFunction::Url(ref url) => url.as_str(),
            DocumentMatchingFunction::UrlPrefix(ref pat)
            | DocumentMatchingFunction::Domain(ref pat)
            | DocumentMatchingFunction::Regexp(ref pat) => pat,
            DocumentMatchingFunction::MediaDocument(kind) => match kind {
                MediaDocumentKind::All => "all",
                MediaDocumentKind::Image => "image",
                MediaDocumentKind::Video => "video",
            },
            DocumentMatchingFunction::PlainTextDocument(())
            | DocumentMatchingFunction::UnobservableDocument(()) => "",
        });
        unsafe { Gecko_DocumentRule_UseForPresentation(device.document(), &*pattern, func) }
    }

    #[cfg(not(feature = "gecko"))]
    /// Evaluate a URL matching function.
    pub fn evaluate(&self, _: &Device) -> bool {
        false
    }
}

/// A `@document` rule's condition.
///
/// <https://www.w3.org/TR/2012/WD-css3-conditional-20120911/#at-document>
///
/// The `@document` rule's condition is written as a comma-separated list of
/// URL matching functions, and the condition evaluates to true whenever any
/// one of those functions evaluates to true.
#[derive(Clone, Debug, ToCss, ToShmem)]
#[css(comma)]
pub struct DocumentCondition(#[css(iterable)] Vec<DocumentMatchingFunction>);

impl DocumentCondition {
    /// Parse a document condition.
    pub fn parse<'i, 't>(
        context: &ParserContext,
        input: &mut Parser<'i, 't>,
    ) -> Result<Self, ParseError<'i>> {
        let conditions =
            input.parse_comma_separated(|input| DocumentMatchingFunction::parse(context, input))?;

        let condition = DocumentCondition(conditions);
        if !condition.allowed_in(context) {
            return Err(input.new_error(BasicParseErrorKind::AtRuleInvalid("-moz-document".into())));
        }
        Ok(condition)
    }

    /// Evaluate a document condition.
    pub fn evaluate(&self, device: &Device) -> bool {
        self.0
            .iter()
            .any(|url_matching_function| url_matching_function.evaluate(device))
    }

    #[cfg(feature = "servo")]
    fn allowed_in(&self, _: &ParserContext) -> bool {
        false
    }

    #[cfg(feature = "gecko")]
    fn allowed_in(&self, context: &ParserContext) -> bool {
        if context.chrome_rules_enabled() {
            return true;
        }

        // Allow a single url-prefix() for compatibility.
        //
        // See bug 1446470 and dependencies.
        if self.0.len() != 1 {
            return false;
        }

        // NOTE(emilio): This technically allows url-prefix("") too, but...
        match self.0[0] {
            DocumentMatchingFunction::UrlPrefix(ref prefix) => prefix.is_empty(),
            _ => false,
        }
    }
}
