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

//! CSS handling for the specified value of
//! [`basic-shape`][basic-shape]s
//!
//! [basic-shape]: https://drafts.csswg.org/css-shapes/#typedef-basic-shape

use crate::parser::{Parse, ParserContext};
use crate::values::computed::basic_shape::InsetRect as ComputedInsetRect;
use crate::values::computed::{
    Context, LengthPercentage as ComputedLengthPercentage, ToComputedValue,
};
use crate::values::generics::basic_shape as generic;
use crate::values::generics::basic_shape::{Path, PolygonCoord};
use crate::values::generics::position::GenericPositionOrAuto;
use crate::values::generics::rect::Rect;
use crate::values::specified::angle::Angle;
use crate::values::specified::border::BorderRadius;
use crate::values::specified::image::Image;
use crate::values::specified::length::LengthPercentageOrAuto;
use crate::values::specified::position::{Position, Side};
use crate::values::specified::url::SpecifiedUrl;
use crate::values::specified::PositionComponent;
use crate::values::specified::{LengthPercentage, NonNegativeLengthPercentage, SVGPathData};
use crate::values::CSSFloat;
use crate::Zero;
use cssparser::Parser;
use std::fmt::{self, Write};
use style_traits::{CssWriter, ParseError, StyleParseErrorKind, ToCss};

/// A specified alias for FillRule.
pub use crate::values::generics::basic_shape::FillRule;

/// A specified `clip-path` value.
pub type ClipPath = generic::GenericClipPath<BasicShape, SpecifiedUrl>;

/// A specified `shape-outside` value.
pub type ShapeOutside = generic::GenericShapeOutside<BasicShape, Image>;

/// A specified value for `at <position>` in circle() and ellipse().
// Note: its computed value is the same as computed::position::Position. We just want to always use
// LengthPercentage as the type of its components, for basic shapes.
pub type RadialPosition = generic::ShapePosition<LengthPercentage>;

/// A specified basic shape.
pub type BasicShape = generic::GenericBasicShape<Angle, Position, LengthPercentage, BasicShapeRect>;

/// The specified value of `inset()`.
pub type InsetRect = generic::GenericInsetRect<LengthPercentage>;

/// A specified circle.
pub type Circle = generic::Circle<LengthPercentage>;

/// A specified ellipse.
pub type Ellipse = generic::Ellipse<LengthPercentage>;

/// The specified value of `ShapeRadius`.
pub type ShapeRadius = generic::ShapeRadius<LengthPercentage>;

/// The specified value of `Polygon`.
pub type Polygon = generic::GenericPolygon<LengthPercentage>;

/// The specified value of `PathOrShapeFunction`.
pub type PathOrShapeFunction =
    generic::GenericPathOrShapeFunction<Angle, Position, LengthPercentage>;

/// The specified value of `ShapeCommand`.
pub type ShapeCommand = generic::GenericShapeCommand<Angle, Position, LengthPercentage>;

/// The specified value of `xywh()`.
/// Defines a rectangle via offsets from the top and left edge of the reference box, and a
/// specified width and height.
///
/// The four <length-percentage>s define, respectively, the inset from the left edge of the
/// reference box, the inset from the top edge of the reference box, the width of the rectangle,
/// and the height of the rectangle.
///
/// https://drafts.csswg.org/css-shapes-1/#funcdef-basic-shape-xywh
#[derive(Clone, Debug, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToShmem)]
pub struct Xywh {
    /// The left edge of the reference box.
    pub x: LengthPercentage,
    /// The top edge of the reference box.
    pub y: LengthPercentage,
    /// The specified width.
    pub width: NonNegativeLengthPercentage,
    /// The specified height.
    pub height: NonNegativeLengthPercentage,
    /// The optional <border-radius> argument(s) define rounded corners for the inset rectangle
    /// using the border-radius shorthand syntax.
    pub round: BorderRadius,
}

/// Defines a rectangle via insets from the top and left edges of the reference box.
///
/// https://drafts.csswg.org/css-shapes-1/#funcdef-basic-shape-rect
#[derive(Clone, Debug, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToShmem)]
#[repr(C)]
pub struct ShapeRectFunction {
    /// The four <length-percentage>s define the position of the top, right, bottom, and left edges
    /// of a rectangle, respectively, as insets from the top edge of the reference box (for the
    /// first and third values) or the left edge of the reference box (for the second and fourth
    /// values).
    ///
    /// An auto value makes the edge of the box coincide with the corresponding edge of the
    /// reference box: it’s equivalent to 0% as the first (top) or fourth (left) value, and
    /// equivalent to 100% as the second (right) or third (bottom) value.
    pub rect: Rect<LengthPercentageOrAuto>,
    /// The optional <border-radius> argument(s) define rounded corners for the inset rectangle
    /// using the border-radius shorthand syntax.
    pub round: BorderRadius,
}

/// The specified value of <basic-shape-rect>.
/// <basic-shape-rect> = <inset()> | <rect()> | <xywh()>
///
/// https://drafts.csswg.org/css-shapes-1/#supported-basic-shapes
#[derive(Clone, Debug, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToCss, ToShmem)]
pub enum BasicShapeRect {
    /// Defines an inset rectangle via insets from each edge of the reference box.
    Inset(InsetRect),
    /// Defines a xywh function.
    #[css(function)]
    Xywh(Xywh),
    /// Defines a rect function.
    #[css(function)]
    Rect(ShapeRectFunction),
}

/// For filled shapes, we use fill-rule, and store it for path() and polygon().
/// For outline shapes, we should ignore fill-rule.
///
/// https://github.com/w3c/fxtf-drafts/issues/512
/// https://github.com/w3c/csswg-drafts/issues/7390
/// https://github.com/w3c/csswg-drafts/issues/3468
pub enum ShapeType {
    /// The CSS property uses filled shapes. The default behavior.
    Filled,
    /// The CSS property uses outline shapes. This is especially useful for offset-path.
    Outline,
}

bitflags! {
    /// The flags to represent which basic shapes we would like to support.
    ///
    /// Different properties may use different subsets of <basic-shape>:
    /// e.g.
    /// clip-path: all basic shapes.
    /// motion-path: all basic shapes (but ignore fill-rule).
    /// shape-outside: inset(), circle(), ellipse(), polygon().
    ///
    /// Also there are some properties we don't support for now:
    /// shape-inside: inset(), circle(), ellipse(), polygon().
    /// SVG shape-inside and shape-subtract: circle(), ellipse(), polygon().
    ///
    /// The spec issue proposes some better ways to clarify the usage of basic shapes, so for now
    /// we use the bitflags to choose the supported basic shapes for each property at the parse
    /// time.
    /// https://github.com/w3c/csswg-drafts/issues/7390
    #[derive(Clone, Copy)]
    #[repr(C)]
    pub struct AllowedBasicShapes: u8 {
        /// inset().
        const INSET = 1 << 0;
        /// xywh().
        const XYWH = 1 << 1;
        /// rect().
        const RECT = 1 << 2;
        /// circle().
        const CIRCLE = 1 << 3;
        /// ellipse().
        const ELLIPSE = 1 << 4;
        /// polygon().
        const POLYGON = 1 << 5;
        /// path().
        const PATH = 1 << 6;
        /// shape().
        const SHAPE = 1 << 7;

        /// All flags.
        const ALL =
            Self::INSET.bits() |
            Self::XYWH.bits() |
            Self::RECT.bits() |
            Self::CIRCLE.bits() |
            Self::ELLIPSE.bits() |
            Self::POLYGON.bits() |
            Self::PATH.bits() |
            Self::SHAPE.bits();

        /// For shape-outside.
        const SHAPE_OUTSIDE =
            Self::INSET.bits() |
            Self::CIRCLE.bits() |
            Self::ELLIPSE.bits() |
            Self::POLYGON.bits();
    }
}

/// A helper for both clip-path and shape-outside parsing of shapes.
fn parse_shape_or_box<'i, 't, R, ReferenceBox>(
    context: &ParserContext,
    input: &mut Parser<'i, 't>,
    to_shape: impl FnOnce(Box<BasicShape>, ReferenceBox) -> R,
    to_reference_box: impl FnOnce(ReferenceBox) -> R,
    flags: AllowedBasicShapes,
) -> Result<R, ParseError<'i>>
where
    ReferenceBox: Default + Parse,
{
    let mut shape = None;
    let mut ref_box = None;
    loop {
        if shape.is_none() {
            shape = input
                .try_parse(|i| BasicShape::parse(context, i, flags, ShapeType::Filled))
                .ok();
        }

        if ref_box.is_none() {
            ref_box = input.try_parse(|i| ReferenceBox::parse(context, i)).ok();
            if ref_box.is_some() {
                continue;
            }
        }
        break;
    }

    if let Some(shp) = shape {
        return Ok(to_shape(Box::new(shp), ref_box.unwrap_or_default()));
    }

    match ref_box {
        Some(r) => Ok(to_reference_box(r)),
        None => Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)),
    }
}

impl Parse for ClipPath {
    #[inline]
    fn parse<'i, 't>(
        context: &ParserContext,
        input: &mut Parser<'i, 't>,
    ) -> Result<Self, ParseError<'i>> {
        if input.try_parse(|i| i.expect_ident_matching("none")).is_ok() {
            return Ok(ClipPath::None);
        }

        if let Ok(url) = input.try_parse(|i| SpecifiedUrl::parse(context, i)) {
            return Ok(ClipPath::Url(url));
        }

        parse_shape_or_box(
            context,
            input,
            ClipPath::Shape,
            ClipPath::Box,
            AllowedBasicShapes::ALL,
        )
    }
}

impl Parse for ShapeOutside {
    #[inline]
    fn parse<'i, 't>(
        context: &ParserContext,
        input: &mut Parser<'i, 't>,
    ) -> Result<Self, ParseError<'i>> {
        // Need to parse this here so that `Image::parse_with_cors_anonymous`
        // doesn't parse it.
        if input.try_parse(|i| i.expect_ident_matching("none")).is_ok() {
            return Ok(ShapeOutside::None);
        }

        if let Ok(image) = input.try_parse(|i| Image::parse_with_cors_anonymous(context, i)) {
            debug_assert_ne!(image, Image::None);
            return Ok(ShapeOutside::Image(image));
        }

        parse_shape_or_box(
            context,
            input,
            ShapeOutside::Shape,
            ShapeOutside::Box,
            AllowedBasicShapes::SHAPE_OUTSIDE,
        )
    }
}

impl BasicShape {
    /// Parse with some parameters.
    /// 1. The supported <basic-shape>.
    /// 2. The type of shapes. Should we ignore fill-rule?
    /// 3. The default value of `at <position>`.
    pub fn parse<'i, 't>(
        context: &ParserContext,
        input: &mut Parser<'i, 't>,
        flags: AllowedBasicShapes,
        shape_type: ShapeType,
    ) -> Result<Self, ParseError<'i>> {
        let location = input.current_source_location();
        let function = input.expect_function()?.clone();
        input.parse_nested_block(move |i| {
            match_ignore_ascii_case! { &function,
                "inset" if flags.contains(AllowedBasicShapes::INSET) => {
                    InsetRect::parse_function_arguments(context, i)
                        .map(BasicShapeRect::Inset)
                        .map(BasicShape::Rect)
                },
                "xywh" if flags.contains(AllowedBasicShapes::XYWH) => {
                    Xywh::parse_function_arguments(context, i)
                        .map(BasicShapeRect::Xywh)
                        .map(BasicShape::Rect)
                },
                "rect" if flags.contains(AllowedBasicShapes::RECT) => {
                    ShapeRectFunction::parse_function_arguments(context, i)
                        .map(BasicShapeRect::Rect)
                        .map(BasicShape::Rect)
                },
                "circle" if flags.contains(AllowedBasicShapes::CIRCLE) => {
                    Circle::parse_function_arguments(context, i)
                        .map(BasicShape::Circle)
                },
                "ellipse" if flags.contains(AllowedBasicShapes::ELLIPSE) => {
                    Ellipse::parse_function_arguments(context, i)
                        .map(BasicShape::Ellipse)
                },
                "polygon" if flags.contains(AllowedBasicShapes::POLYGON) => {
                    Polygon::parse_function_arguments(context, i, shape_type)
                        .map(BasicShape::Polygon)
                },
                "path" if flags.contains(AllowedBasicShapes::PATH) => {
                    Path::parse_function_arguments(i, shape_type)
                        .map(PathOrShapeFunction::Path)
                        .map(BasicShape::PathOrShape)
                },
                "shape"
                    if flags.contains(AllowedBasicShapes::SHAPE)
                        && static_prefs::pref!("layout.css.basic-shape-shape.enabled") =>
                {
                    generic::Shape::parse_function_arguments(context, i, shape_type)
                        .map(PathOrShapeFunction::Shape)
                        .map(BasicShape::PathOrShape)
                },
                _ => Err(location
                    .new_custom_error(StyleParseErrorKind::UnexpectedFunction(function.clone()))),
            }
        })
    }
}

impl Parse for InsetRect {
    fn parse<'i, 't>(
        context: &ParserContext,
        input: &mut Parser<'i, 't>,
    ) -> Result<Self, ParseError<'i>> {
        input.expect_function_matching("inset")?;
        input.parse_nested_block(|i| Self::parse_function_arguments(context, i))
    }
}

fn parse_round<'i, 't>(
    context: &ParserContext,
    input: &mut Parser<'i, 't>,
) -> Result<BorderRadius, ParseError<'i>> {
    if input
        .try_parse(|i| i.expect_ident_matching("round"))
        .is_ok()
    {
        return BorderRadius::parse(context, input);
    }

    Ok(BorderRadius::zero())
}

impl InsetRect {
    /// Parse the inner function arguments of `inset()`
    fn parse_function_arguments<'i, 't>(
        context: &ParserContext,
        input: &mut Parser<'i, 't>,
    ) -> Result<Self, ParseError<'i>> {
        let rect = Rect::parse_with(context, input, LengthPercentage::parse)?;
        let round = parse_round(context, input)?;
        Ok(generic::InsetRect { rect, round })
    }
}

impl ToCss for RadialPosition {
    fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
    where
        W: Write,
    {
        self.horizontal.to_css(dest)?;
        dest.write_char(' ')?;
        self.vertical.to_css(dest)
    }
}

fn convert_to_length_percentage<S: Side>(c: PositionComponent<S>) -> LengthPercentage {
    use crate::values::specified::{AllowedNumericType, Percentage};
    // Convert the value when parsing, to make sure we serialize it properly for both
    // specified and computed values.
    // https://drafts.csswg.org/css-shapes-1/#basic-shape-serialization
    match c {
        // Since <position> keywords stand in for percentages, keywords without an offset
        // turn into percentages.
        PositionComponent::Center => LengthPercentage::from(Percentage::new(0.5)),
        PositionComponent::Side(keyword, None) => {
            Percentage::new(if keyword.is_start() { 0. } else { 1. }).into()
        },
        // Per spec issue, https://github.com/w3c/csswg-drafts/issues/8695, the part of
        // "avoiding calc() expressions where possible" and "avoiding calc()
        // transformations" will be removed from the spec, and we should follow the
        // css-values-4 for position, i.e. we make it as length-percentage always.
        // https://drafts.csswg.org/css-shapes-1/#basic-shape-serialization.
        // https://drafts.csswg.org/css-values-4/#typedef-position
        PositionComponent::Side(keyword, Some(length)) => {
            if keyword.is_start() {
                length
            } else {
                length.hundred_percent_minus(AllowedNumericType::All)
            }
        },
        PositionComponent::Length(length) => length,
    }
}

fn parse_at_position<'i, 't>(
    context: &ParserContext,
    input: &mut Parser<'i, 't>,
) -> Result<GenericPositionOrAuto<RadialPosition>, ParseError<'i>> {
    use crate::values::specified::position::Position;
    if input.try_parse(|i| i.expect_ident_matching("at")).is_ok() {
        Position::parse(context, input).map(|pos| {
            GenericPositionOrAuto::Position(RadialPosition::new(
                convert_to_length_percentage(pos.horizontal),
                convert_to_length_percentage(pos.vertical),
            ))
        })
    } else {
        // `at <position>` is omitted.
        Ok(GenericPositionOrAuto::Auto)
    }
}

impl Parse for Circle {
    fn parse<'i, 't>(
        context: &ParserContext,
        input: &mut Parser<'i, 't>,
    ) -> Result<Self, ParseError<'i>> {
        input.expect_function_matching("circle")?;
        input.parse_nested_block(|i| Self::parse_function_arguments(context, i))
    }
}

impl Circle {
    fn parse_function_arguments<'i, 't>(
        context: &ParserContext,
        input: &mut Parser<'i, 't>,
    ) -> Result<Self, ParseError<'i>> {
        let radius = input
            .try_parse(|i| ShapeRadius::parse(context, i))
            .unwrap_or_default();
        let position = parse_at_position(context, input)?;

        Ok(generic::Circle { radius, position })
    }
}

impl Parse for Ellipse {
    fn parse<'i, 't>(
        context: &ParserContext,
        input: &mut Parser<'i, 't>,
    ) -> Result<Self, ParseError<'i>> {
        input.expect_function_matching("ellipse")?;
        input.parse_nested_block(|i| Self::parse_function_arguments(context, i))
    }
}

impl Ellipse {
    fn parse_function_arguments<'i, 't>(
        context: &ParserContext,
        input: &mut Parser<'i, 't>,
    ) -> Result<Self, ParseError<'i>> {
        let (semiaxis_x, semiaxis_y) = input
            .try_parse(|i| -> Result<_, ParseError> {
                Ok((
                    ShapeRadius::parse(context, i)?,
                    ShapeRadius::parse(context, i)?,
                ))
            })
            .unwrap_or_default();
        let position = parse_at_position(context, input)?;

        Ok(generic::Ellipse {
            semiaxis_x,
            semiaxis_y,
            position,
        })
    }
}

fn parse_fill_rule<'i, 't>(
    input: &mut Parser<'i, 't>,
    shape_type: ShapeType,
    expect_comma: bool,
) -> FillRule {
    match shape_type {
        // Per [1] and [2], we ignore `<fill-rule>` for outline shapes, so always use a default
        // value.
        // [1] https://github.com/w3c/csswg-drafts/issues/3468
        // [2] https://github.com/w3c/csswg-drafts/issues/7390
        //
        // Also, per [3] and [4], we would like the ignore `<file-rule>` from outline shapes, e.g.
        // offset-path, which means we don't parse it when setting `ShapeType::Outline`.
        // This should be web compatible because the shipped "offset-path:path()" doesn't have
        // `<fill-rule>` and "offset-path:polygon()" is a new feature and still behind the
        // preference.
        // [3] https://github.com/w3c/fxtf-drafts/issues/512#issuecomment-1545393321
        // [4] https://github.com/w3c/fxtf-drafts/issues/512#issuecomment-1555330929
        ShapeType::Outline => Default::default(),
        ShapeType::Filled => input
            .try_parse(|i| -> Result<_, ParseError> {
                let fill = FillRule::parse(i)?;
                if expect_comma {
                    i.expect_comma()?;
                }
                Ok(fill)
            })
            .unwrap_or_default(),
    }
}

impl Parse for Polygon {
    fn parse<'i, 't>(
        context: &ParserContext,
        input: &mut Parser<'i, 't>,
    ) -> Result<Self, ParseError<'i>> {
        input.expect_function_matching("polygon")?;
        input.parse_nested_block(|i| Self::parse_function_arguments(context, i, ShapeType::Filled))
    }
}

impl Polygon {
    /// Parse the inner arguments of a `polygon` function.
    fn parse_function_arguments<'i, 't>(
        context: &ParserContext,
        input: &mut Parser<'i, 't>,
        shape_type: ShapeType,
    ) -> Result<Self, ParseError<'i>> {
        let fill = parse_fill_rule(input, shape_type, true /* has comma */);
        let coordinates = input
            .parse_comma_separated(|i| {
                Ok(PolygonCoord(
                    LengthPercentage::parse(context, i)?,
                    LengthPercentage::parse(context, i)?,
                ))
            })?
            .into();

        Ok(Polygon { fill, coordinates })
    }
}

impl Path {
    /// Parse the inner arguments of a `path` function.
    fn parse_function_arguments<'i, 't>(
        input: &mut Parser<'i, 't>,
        shape_type: ShapeType,
    ) -> Result<Self, ParseError<'i>> {
        use crate::values::specified::svg_path::AllowEmpty;

        let fill = parse_fill_rule(input, shape_type, true /* has comma */);
        let path = SVGPathData::parse(input, AllowEmpty::No)?;
        Ok(Path { fill, path })
    }
}

fn round_to_css<W>(round: &BorderRadius, dest: &mut CssWriter<W>) -> fmt::Result
where
    W: Write,
{
    if !round.is_zero() {
        dest.write_str(" round ")?;
        round.to_css(dest)?;
    }
    Ok(())
}

impl ToCss for Xywh {
    fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
    where
        W: Write,
    {
        self.x.to_css(dest)?;
        dest.write_char(' ')?;
        self.y.to_css(dest)?;
        dest.write_char(' ')?;
        self.width.to_css(dest)?;
        dest.write_char(' ')?;
        self.height.to_css(dest)?;
        round_to_css(&self.round, dest)
    }
}

impl Xywh {
    /// Parse the inner function arguments of `xywh()`.
    fn parse_function_arguments<'i, 't>(
        context: &ParserContext,
        input: &mut Parser<'i, 't>,
    ) -> Result<Self, ParseError<'i>> {
        let x = LengthPercentage::parse(context, input)?;
        let y = LengthPercentage::parse(context, input)?;
        let width = NonNegativeLengthPercentage::parse(context, input)?;
        let height = NonNegativeLengthPercentage::parse(context, input)?;
        let round = parse_round(context, input)?;
        Ok(Xywh {
            x,
            y,
            width,
            height,
            round,
        })
    }
}

impl ToCss for ShapeRectFunction {
    fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
    where
        W: Write,
    {
        self.rect.0.to_css(dest)?;
        dest.write_char(' ')?;
        self.rect.1.to_css(dest)?;
        dest.write_char(' ')?;
        self.rect.2.to_css(dest)?;
        dest.write_char(' ')?;
        self.rect.3.to_css(dest)?;
        round_to_css(&self.round, dest)
    }
}

impl ShapeRectFunction {
    /// Parse the inner function arguments of `rect()`.
    fn parse_function_arguments<'i, 't>(
        context: &ParserContext,
        input: &mut Parser<'i, 't>,
    ) -> Result<Self, ParseError<'i>> {
        let rect = Rect::parse_all_components_with(context, input, LengthPercentageOrAuto::parse)?;
        let round = parse_round(context, input)?;
        Ok(ShapeRectFunction { rect, round })
    }
}

impl ToComputedValue for BasicShapeRect {
    type ComputedValue = ComputedInsetRect;

    #[inline]
    fn to_computed_value(&self, context: &Context) -> Self::ComputedValue {
        use crate::values::computed::LengthPercentage;
        use crate::values::computed::LengthPercentageOrAuto;
        use style_traits::values::specified::AllowedNumericType;

        match self {
            Self::Inset(ref inset) => inset.to_computed_value(context),
            Self::Xywh(ref xywh) => {
                // Given `xywh(x y w h)`, construct the equivalent inset() function,
                // `inset(y calc(100% - x - w) calc(100% - y - h) x)`.
                //
                // https://drafts.csswg.org/css-shapes-1/#basic-shape-computed-values
                // https://github.com/w3c/csswg-drafts/issues/9053
                let x = xywh.x.to_computed_value(context);
                let y = xywh.y.to_computed_value(context);
                let w = xywh.width.to_computed_value(context);
                let h = xywh.height.to_computed_value(context);
                // calc(100% - x - w).
                let right = LengthPercentage::hundred_percent_minus_list(
                    &[&x, &w.0],
                    AllowedNumericType::All,
                );
                // calc(100% - y - h).
                let bottom = LengthPercentage::hundred_percent_minus_list(
                    &[&y, &h.0],
                    AllowedNumericType::All,
                );

                ComputedInsetRect {
                    rect: Rect::new(y, right, bottom, x),
                    round: xywh.round.to_computed_value(context),
                }
            },
            Self::Rect(ref rect) => {
                // Given `rect(t r b l)`, the equivalent function is
                // `inset(t calc(100% - r) calc(100% - b) l)`.
                //
                // https://drafts.csswg.org/css-shapes-1/#basic-shape-computed-values
                fn compute_top_or_left(v: LengthPercentageOrAuto) -> LengthPercentage {
                    match v {
                        // it’s equivalent to 0% as the first (top) or fourth (left) value.
                        // https://drafts.csswg.org/css-shapes-1/#funcdef-basic-shape-rect
                        LengthPercentageOrAuto::Auto => LengthPercentage::zero_percent(),
                        LengthPercentageOrAuto::LengthPercentage(lp) => lp,
                    }
                }
                fn compute_bottom_or_right(v: LengthPercentageOrAuto) -> LengthPercentage {
                    match v {
                        // It's equivalent to 100% as the second (right) or third (bottom) value.
                        // So calc(100% - 100%) = 0%.
                        // https://drafts.csswg.org/css-shapes-1/#funcdef-basic-shape-rect
                        LengthPercentageOrAuto::Auto => LengthPercentage::zero_percent(),
                        LengthPercentageOrAuto::LengthPercentage(lp) => {
                            LengthPercentage::hundred_percent_minus(lp, AllowedNumericType::All)
                        },
                    }
                }

                let round = rect.round.to_computed_value(context);
                let rect = rect.rect.to_computed_value(context);
                let rect = Rect::new(
                    compute_top_or_left(rect.0),
                    compute_bottom_or_right(rect.1),
                    compute_bottom_or_right(rect.2),
                    compute_top_or_left(rect.3),
                );

                ComputedInsetRect { rect, round }
            },
        }
    }

    #[inline]
    fn from_computed_value(computed: &Self::ComputedValue) -> Self {
        Self::Inset(ToComputedValue::from_computed_value(computed))
    }
}

impl generic::Shape<Angle, Position, LengthPercentage> {
    /// Parse the inner arguments of a `shape` function.
    /// shape() = shape(<fill-rule>? from <coordinate-pair>, <shape-command>#)
    fn parse_function_arguments<'i, 't>(
        context: &ParserContext,
        input: &mut Parser<'i, 't>,
        shape_type: ShapeType,
    ) -> Result<Self, ParseError<'i>> {
        let fill = parse_fill_rule(input, shape_type, false /* no following comma */);

        let mut first = true;
        let commands = input.parse_comma_separated(|i| {
            if first {
                first = false;

                // The starting point for the first shape-command. It adds an initial absolute
                // moveto to the list of path data commands, with the <coordinate-pair> measured
                // from the top-left corner of the reference
                i.expect_ident_matching("from")?;
                Ok(ShapeCommand::Move {
                    point: generic::CommandEndPoint::parse_endpoint_as_abs(context, i)?,
                })
            } else {
                // The further path data commands.
                ShapeCommand::parse(context, i)
            }
        })?;

        // We must have one starting point and at least one following <shape-command>.
        if commands.len() < 2 {
            return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError));
        }

        Ok(Self {
            fill,
            commands: commands.into(),
        })
    }
}

impl Parse for ShapeCommand {
    fn parse<'i, 't>(
        context: &ParserContext,
        input: &mut Parser<'i, 't>,
    ) -> Result<Self, ParseError<'i>> {
        use crate::values::generics::basic_shape::{
            ArcRadii, ArcSize, ArcSweep, AxisEndPoint, CommandEndPoint, ControlPoint,
        };

        // <shape-command> = <move-command> | <line-command> | <hv-line-command> |
        //                   <curve-command> | <smooth-command> | <arc-command> | close
        Ok(try_match_ident_ignore_ascii_case! { input,
            "close" => Self::Close,
            "move" => {
                let point = CommandEndPoint::parse(context, input)?;
                Self::Move { point }
            },
            "line" => {
                let point = CommandEndPoint::parse(context, input)?;
                Self::Line { point }
            },
            "hline" => {
                let x = AxisEndPoint::parse_hline(context, input)?;
                Self::HLine { x }
            },
            "vline" => {
                let y = AxisEndPoint::parse_vline(context, input)?;
                Self::VLine { y }
            },
            "curve" => {
                let point = CommandEndPoint::parse(context, input)?;
                input.expect_ident_matching("with")?;
                let control1 = ControlPoint::parse(context, input, point.is_abs())?;
                if input.try_parse(|i| i.expect_delim('/')).is_ok() {
                    let control2 = ControlPoint::parse(context, input, point.is_abs())?;
                    Self::CubicCurve {
                        point,
                        control1,
                        control2,
                    }
                } else {
                    Self::QuadCurve {
                        point,
                        control1,
                    }
                }
            },
            "smooth" => {
                let point = CommandEndPoint::parse(context, input)?;
                if input.try_parse(|i| i.expect_ident_matching("with")).is_ok() {
                    let control2 = ControlPoint::parse(context, input, point.is_abs())?;
                    Self::SmoothCubic {
                        point,
                        control2,
                    }
                } else {
                    Self::SmoothQuad { point }
                }
            },
            "arc" => {
                let point = CommandEndPoint::parse(context, input)?;
                input.expect_ident_matching("of")?;
                let rx = LengthPercentage::parse(context, input)?;
                let ry = input.try_parse(|i| LengthPercentage::parse(context, i)).ok();
                let radii = ArcRadii { rx, ry: ry.into() };

                // [<arc-sweep> || <arc-size> || rotate <angle>]?
                let mut arc_sweep = None;
                let mut arc_size = None;
                let mut rotate = None;
                loop {
                    if arc_sweep.is_none() {
                        arc_sweep = input.try_parse(ArcSweep::parse).ok();
                    }

                    if arc_size.is_none() {
                        arc_size = input.try_parse(ArcSize::parse).ok();
                        if arc_size.is_some() {
                            continue;
                        }
                    }

                    if rotate.is_none()
                        && input
                            .try_parse(|i| i.expect_ident_matching("rotate"))
                            .is_ok()
                    {
                        rotate = Some(Angle::parse(context, input)?);
                        continue;
                    }
                    break;
                }
                Self::Arc {
                    point,
                    radii,
                    arc_sweep: arc_sweep.unwrap_or(ArcSweep::Ccw),
                    arc_size: arc_size.unwrap_or(ArcSize::Small),
                    rotate: rotate.unwrap_or(Angle::zero()),
                }
            },
        })
    }
}

impl Parse for generic::CoordinatePair<LengthPercentage> {
    fn parse<'i, 't>(
        context: &ParserContext,
        input: &mut Parser<'i, 't>,
    ) -> Result<Self, ParseError<'i>> {
        let x = LengthPercentage::parse(context, input)?;
        let y = LengthPercentage::parse(context, input)?;
        Ok(Self::new(x, y))
    }
}

impl generic::ControlPoint<Position, LengthPercentage> {
    /// Parse <control-point> = [ <position> | <relative-control-point> ]
    fn parse<'i, 't>(
        context: &ParserContext,
        input: &mut Parser<'i, 't>,
        is_end_point_abs: bool,
    ) -> Result<Self, ParseError<'i>> {
        use generic::ControlReference;
        let coord = input.try_parse(|i| generic::CoordinatePair::parse(context, i));

        // Parse <position>
        if is_end_point_abs && coord.is_err() {
            let pos = Position::parse(context, input)?;
            return Ok(Self::Absolute(pos));
        }

        // Parse <relative-control-point> = <coordinate-pair> [from [ start | end | origin ]]?
        let coord = coord?;
        let mut reference = if is_end_point_abs {
            ControlReference::Origin
        } else {
            ControlReference::Start
        };
        if input.try_parse(|i| i.expect_ident_matching("from")).is_ok() {
            reference = ControlReference::parse(input)?;
        }

        Ok(Self::Relative(generic::RelativeControlPoint {
            coord,
            reference,
        }))
    }
}

impl Parse for generic::CommandEndPoint<Position, LengthPercentage> {
    /// Parse <command-end-point> = to <position> | by <coordinate-pair>
    fn parse<'i, 't>(
        context: &ParserContext,
        input: &mut Parser<'i, 't>,
    ) -> Result<Self, ParseError<'i>> {
        if ByTo::parse(input)?.is_abs() {
            Self::parse_endpoint_as_abs(context, input)
        } else {
            let point = generic::CoordinatePair::parse(context, input)?;
            Ok(Self::ByCoordinate(point))
        }
    }
}

impl generic::CommandEndPoint<Position, LengthPercentage> {
    /// Parse <command-end-point> = to <position>
    fn parse_endpoint_as_abs<'i, 't>(
        context: &ParserContext,
        input: &mut Parser<'i, 't>,
    ) -> Result<Self, ParseError<'i>> {
        let point = Position::parse(context, input)?;
        Ok(generic::CommandEndPoint::ToPosition(point))
    }
}

impl generic::AxisEndPoint<LengthPercentage> {
    /// Parse <horizontal-line-command>
    pub fn parse_hline<'i, 't>(
        context: &ParserContext,
        input: &mut Parser<'i, 't>,
    ) -> Result<Self, ParseError<'i>> {
        use cssparser::Token;
        use generic::{AxisPosition, AxisPositionKeyword};

        // If the command is relative, parse for <length-percentage> only.
        if !ByTo::parse(input)?.is_abs() {
            return Ok(Self::ByCoordinate(LengthPercentage::parse(context, input)?));
        }

        let x = AxisPosition::parse(context, input)?;
        if let AxisPosition::Keyword(
            _word @ (AxisPositionKeyword::Top
            | AxisPositionKeyword::Bottom
            | AxisPositionKeyword::YStart
            | AxisPositionKeyword::YEnd),
        ) = &x
        {
            let location = input.current_source_location();
            let token = Token::Ident(x.to_css_string().into());
            return Err(location.new_unexpected_token_error(token));
        }
        Ok(Self::ToPosition(x))
    }

    /// Parse <vertical-line-command>
    pub fn parse_vline<'i, 't>(
        context: &ParserContext,
        input: &mut Parser<'i, 't>,
    ) -> Result<Self, ParseError<'i>> {
        use cssparser::Token;
        use generic::{AxisPosition, AxisPositionKeyword};

        // If the command is relative, parse for <length-percentage> only.
        if !ByTo::parse(input)?.is_abs() {
            return Ok(Self::ByCoordinate(LengthPercentage::parse(context, input)?));
        }

        let y = AxisPosition::parse(context, input)?;
        if let AxisPosition::Keyword(
            _word @ (AxisPositionKeyword::Left
            | AxisPositionKeyword::Right
            | AxisPositionKeyword::XStart
            | AxisPositionKeyword::XEnd),
        ) = &y
        {
            // Return an error if we parsed a different keyword.
            let location = input.current_source_location();
            let token = Token::Ident(y.to_css_string().into());
            return Err(location.new_unexpected_token_error(token));
        }
        Ok(Self::ToPosition(y))
    }
}

impl ToComputedValue for generic::AxisPosition<LengthPercentage> {
    type ComputedValue = generic::AxisPosition<ComputedLengthPercentage>;

    fn to_computed_value(&self, context: &Context) -> Self::ComputedValue {
        match self {
            Self::LengthPercent(lp) => {
                Self::ComputedValue::LengthPercent(lp.to_computed_value(context))
            },
            Self::Keyword(word) => {
                let lp = LengthPercentage::Percentage(word.as_percentage());
                Self::ComputedValue::LengthPercent(lp.to_computed_value(context))
            },
        }
    }

    fn from_computed_value(computed: &Self::ComputedValue) -> Self {
        match computed {
            Self::ComputedValue::LengthPercent(lp) => {
                Self::LengthPercent(LengthPercentage::from_computed_value(lp))
            },
            _ => unreachable!("Invalid state: computed value cannot be a keyword."),
        }
    }
}

impl ToComputedValue for generic::AxisPosition<CSSFloat> {
    type ComputedValue = Self;

    fn to_computed_value(&self, _context: &Context) -> Self {
        *self
    }

    fn from_computed_value(computed: &Self) -> Self {
        *computed
    }
}

/// This determines whether the command is absolutely or relatively positioned.
/// https://drafts.csswg.org/css-shapes-1/#typedef-shape-command-end-point
#[derive(Clone, Copy, Debug, Parse, PartialEq)]
enum ByTo {
    /// Command is relative to the command’s starting point.
    By,
    /// Command is relative to the top-left corner of the reference box.
    To,
}

impl ByTo {
    /// Return true if it is absolute, i.e. it is To.
    #[inline]
    pub fn is_abs(&self) -> bool {
        matches!(self, ByTo::To)
    }
}
