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

//! Used for parsing and serializing the [`@property`] syntax string.
//!
//! <https://drafts.css-houdini.org/css-properties-values-api-1/#parsing-syntax>

use std::fmt::{self, Debug};
use std::{borrow::Cow, fmt::Write};

use crate::parser::{Parse, ParserContext};
use crate::values::CustomIdent;
use cssparser::{Parser as CSSParser, ParserInput as CSSParserInput};
use style_traits::{
    CssWriter, ParseError as StyleParseError, PropertySyntaxParseError as ParseError,
    StyleParseErrorKind, ToCss,
};

use self::data_type::{DataType, DependentDataTypes};

mod ascii;
pub mod data_type;

/// <https://drafts.css-houdini.org/css-properties-values-api-1/#parsing-syntax>
#[derive(Debug, Clone, Default, MallocSizeOf, PartialEq)]
pub struct Descriptor {
    /// The parsed components, if any.
    /// TODO: Could be a Box<[]> if that supported const construction.
    pub components: Vec<Component>,
    /// The specified css syntax, if any.
    specified: Option<Box<str>>,
}

impl Descriptor {
    /// Returns the universal descriptor.
    pub const fn universal() -> Self {
        Self {
            components: Vec::new(),
            specified: None,
        }
    }

    /// Returns whether this is the universal syntax descriptor.
    #[inline]
    pub fn is_universal(&self) -> bool {
        self.components.is_empty()
    }

    /// Returns the specified string, if any.
    #[inline]
    pub fn specified_string(&self) -> Option<&str> {
        self.specified.as_deref()
    }

    /// Parse a syntax descriptor.
    /// https://drafts.css-houdini.org/css-properties-values-api-1/#consume-a-syntax-definition
    pub fn from_str(css: &str, save_specified: bool) -> Result<Self, ParseError> {
        // 1. Strip leading and trailing ASCII whitespace from string.
        let input = ascii::trim_ascii_whitespace(css);

        // 2. If string's length is 0, return failure.
        if input.is_empty() {
            return Err(ParseError::EmptyInput);
        }

        let specified = if save_specified {
            Some(Box::from(css))
        } else {
            None
        };

        // 3. If string's length is 1, and the only code point in string is U+002A
        //    ASTERISK (*), return the universal syntax descriptor.
        if input.len() == 1 && input.as_bytes()[0] == b'*' {
            return Ok(Self {
                components: Default::default(),
                specified,
            });
        }

        // 4. Let stream be an input stream created from the code points of string,
        //    preprocessed as specified in [css-syntax-3]. Let descriptor be an
        //    initially empty list of syntax components.
        //
        // NOTE(emilio): Instead of preprocessing we cheat and treat new-lines and
        // nulls in the parser specially.
        let mut components = vec![];
        {
            let mut parser = Parser::new(input, &mut components);
            // 5. Repeatedly consume the next input code point from stream.
            parser.parse()?;
        }
        Ok(Self {
            components,
            specified,
        })
    }

    /// Returns the dependent types this syntax might contain.
    pub fn dependent_types(&self) -> DependentDataTypes {
        let mut types = DependentDataTypes::empty();
        for component in self.components.iter() {
            let t = match &component.name {
                ComponentName::DataType(ref t) => t,
                ComponentName::Ident(_) => continue,
            };
            types.insert(t.dependent_types());
        }
        types
    }
}

impl ToCss for Descriptor {
    fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
    where
        W: Write,
    {
        if let Some(ref specified) = self.specified {
            return specified.to_css(dest);
        }

        if self.is_universal() {
            return dest.write_char('*');
        }

        let mut first = true;
        for component in &*self.components {
            if !first {
                dest.write_str(" | ")?;
            }
            component.to_css(dest)?;
            first = false;
        }

        Ok(())
    }
}

impl Parse for Descriptor {
    /// Parse a syntax descriptor.
    fn parse<'i>(
        _: &ParserContext,
        parser: &mut CSSParser<'i, '_>,
    ) -> Result<Self, StyleParseError<'i>> {
        let input = parser.expect_string()?;
        Descriptor::from_str(input.as_ref(), /* save_specified = */ true)
            .map_err(|err| parser.new_custom_error(StyleParseErrorKind::PropertySyntaxField(err)))
    }
}

/// <https://drafts.css-houdini.org/css-properties-values-api-1/#multipliers>
#[derive(
    Clone, Copy, Debug, MallocSizeOf, PartialEq, ToComputedValue, ToResolvedValue, ToShmem,
)]
pub enum Multiplier {
    /// Indicates a space-separated list.
    Space,
    /// Indicates a comma-separated list.
    Comma,
}

impl ToCss for Multiplier {
    fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
    where
        W: Write,
    {
        dest.write_char(match *self {
            Multiplier::Space => '+',
            Multiplier::Comma => '#',
        })
    }
}

/// <https://drafts.css-houdini.org/css-properties-values-api-1/#syntax-component>
#[derive(Clone, Debug, MallocSizeOf, PartialEq)]
pub struct Component {
    name: ComponentName,
    multiplier: Option<Multiplier>,
}

impl Component {
    /// Returns the component's name.
    #[inline]
    pub fn name(&self) -> &ComponentName {
        &self.name
    }

    /// Returns the component's multiplier, if one exists.
    #[inline]
    pub fn multiplier(&self) -> Option<Multiplier> {
        self.multiplier
    }

    /// If the component is premultiplied, return the un-premultiplied component.
    #[inline]
    pub fn unpremultiplied(&self) -> Cow<'_, Self> {
        match self.name.unpremultiply() {
            Some(component) => {
                debug_assert!(
                    self.multiplier.is_none(),
                    "Shouldn't have parsed a multiplier for a pre-multiplied data type name",
                );
                Cow::Owned(component)
            },
            None => Cow::Borrowed(self),
        }
    }
}

impl ToCss for Component {
    fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
    where
        W: Write,
    {
        self.name().to_css(dest)?;
        self.multiplier().to_css(dest)
    }
}

/// <https://drafts.css-houdini.org/css-properties-values-api-1/#syntax-component-name>
#[derive(Clone, Debug, MallocSizeOf, PartialEq, ToCss)]
pub enum ComponentName {
    /// <https://drafts.css-houdini.org/css-properties-values-api-1/#data-type-name>
    DataType(DataType),
    /// <https://drafts.csswg.org/css-values-4/#custom-idents>
    Ident(CustomIdent),
}

impl ComponentName {
    fn unpremultiply(&self) -> Option<Component> {
        match *self {
            ComponentName::DataType(ref t) => t.unpremultiply(),
            ComponentName::Ident(..) => None,
        }
    }

    /// <https://drafts.css-houdini.org/css-properties-values-api-1/#pre-multiplied-data-type-name>
    fn is_pre_multiplied(&self) -> bool {
        self.unpremultiply().is_some()
    }
}

struct Parser<'a> {
    input: &'a str,
    position: usize,
    output: &'a mut Vec<Component>,
}

/// <https://drafts.csswg.org/css-syntax-3/#letter>
fn is_letter(byte: u8) -> bool {
    match byte {
        b'A'..=b'Z' | b'a'..=b'z' => true,
        _ => false,
    }
}

/// <https://drafts.csswg.org/css-syntax-3/#non-ascii-code-point>
fn is_non_ascii(byte: u8) -> bool {
    byte >= 0x80
}

/// <https://drafts.csswg.org/css-syntax-3/#name-start-code-point>
fn is_name_start(byte: u8) -> bool {
    is_letter(byte) || is_non_ascii(byte) || byte == b'_'
}

impl<'a> Parser<'a> {
    fn new(input: &'a str, output: &'a mut Vec<Component>) -> Self {
        Self {
            input,
            position: 0,
            output,
        }
    }

    fn peek(&self) -> Option<u8> {
        self.input.as_bytes().get(self.position).cloned()
    }

    fn parse(&mut self) -> Result<(), ParseError> {
        // 5. Repeatedly consume the next input code point from stream:
        loop {
            let component = self.parse_component()?;
            self.output.push(component);
            self.skip_whitespace();

            let byte = match self.peek() {
                None => return Ok(()),
                Some(b) => b,
            };

            if byte != b'|' {
                return Err(ParseError::ExpectedPipeBetweenComponents);
            }

            self.position += 1;
        }
    }

    fn skip_whitespace(&mut self) {
        loop {
            match self.peek() {
                Some(c) if c.is_ascii_whitespace() => self.position += 1,
                _ => return,
            }
        }
    }

    /// <https://drafts.css-houdini.org/css-properties-values-api-1/#consume-data-type-name>
    fn parse_data_type_name(&mut self) -> Result<DataType, ParseError> {
        let start = self.position;
        loop {
            let byte = match self.peek() {
                Some(b) => b,
                None => return Err(ParseError::UnclosedDataTypeName),
            };
            if byte != b'>' {
                self.position += 1;
                continue;
            }
            let ty = match DataType::from_str(&self.input[start..self.position]) {
                Some(ty) => ty,
                None => return Err(ParseError::UnknownDataTypeName),
            };
            self.position += 1;
            return Ok(ty);
        }
    }

    fn parse_name(&mut self) -> Result<ComponentName, ParseError> {
        let b = match self.peek() {
            Some(b) => b,
            None => return Err(ParseError::UnexpectedEOF),
        };

        if b == b'<' {
            self.position += 1;
            return Ok(ComponentName::DataType(self.parse_data_type_name()?));
        }

        if b != b'\\' && !is_name_start(b) {
            return Err(ParseError::InvalidNameStart);
        }

        let input = &self.input[self.position..];
        let mut input = CSSParserInput::new(input);
        let mut input = CSSParser::new(&mut input);
        let name = match CustomIdent::parse(&mut input, &[]) {
            Ok(name) => name,
            Err(_) => return Err(ParseError::InvalidName),
        };
        self.position += input.position().byte_index();
        return Ok(ComponentName::Ident(name));
    }

    fn parse_multiplier(&mut self) -> Option<Multiplier> {
        let multiplier = match self.peek()? {
            b'+' => Multiplier::Space,
            b'#' => Multiplier::Comma,
            _ => return None,
        };
        self.position += 1;
        Some(multiplier)
    }

    /// <https://drafts.css-houdini.org/css-properties-values-api-1/#consume-a-syntax-component>
    fn parse_component(&mut self) -> Result<Component, ParseError> {
        // Consume as much whitespace as possible from stream.
        self.skip_whitespace();
        let name = self.parse_name()?;
        let multiplier = if name.is_pre_multiplied() {
            None
        } else {
            self.parse_multiplier()
        };
        Ok(Component { name, multiplier })
    }
}
