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

//! Specified types for CSS values that are related to transformations.

use crate::parser::{Parse, ParserContext};
use crate::values::computed::{Context, LengthPercentage as ComputedLengthPercentage};
use crate::values::computed::{Percentage as ComputedPercentage, ToComputedValue};
use crate::values::generics::transform as generic;
use crate::values::generics::transform::{Matrix, Matrix3D};
use crate::values::specified::position::{
    HorizontalPositionKeyword, Side, VerticalPositionKeyword,
};
use crate::values::specified::{
    self, AllowQuirks, Angle, Integer, Length, LengthPercentage, Number, NumberOrPercentage,
};
use crate::Zero;
use cssparser::Parser;
use style_traits::{ParseError, StyleParseErrorKind};

pub use crate::values::generics::transform::TransformStyle;

/// A single operation in a specified CSS `transform`
pub type TransformOperation =
    generic::TransformOperation<Angle, Number, Length, Integer, LengthPercentage>;

/// A specified CSS `transform`
pub type Transform = generic::Transform<TransformOperation>;

/// The specified value of a CSS `<transform-origin>`
pub type TransformOrigin = generic::TransformOrigin<
    OriginComponent<HorizontalPositionKeyword>,
    OriginComponent<VerticalPositionKeyword>,
    Length,
>;

#[cfg(feature = "gecko")]
fn all_transform_boxes_are_enabled(_context: &ParserContext) -> bool {
    true
}

#[cfg(feature = "servo")]
fn all_transform_boxes_are_enabled(_context: &ParserContext) -> bool {
    false
}

/// The specified value of `transform-box`.
/// https://drafts.csswg.org/css-transforms-1/#transform-box
// Note: Once we ship everything, we can drop this and just use single_keyword for tranform-box.
#[allow(missing_docs)]
#[derive(
    Animate,
    Clone,
    ComputeSquaredDistance,
    Copy,
    Debug,
    Deserialize,
    MallocSizeOf,
    Parse,
    PartialEq,
    Serialize,
    SpecifiedValueInfo,
    ToAnimatedValue,
    ToComputedValue,
    ToCss,
    ToResolvedValue,
    ToShmem,
    ToTyped,
)]
#[repr(u8)]
pub enum TransformBox {
    #[parse(condition = "all_transform_boxes_are_enabled")]
    ContentBox,
    BorderBox,
    FillBox,
    #[parse(condition = "all_transform_boxes_are_enabled")]
    StrokeBox,
    ViewBox,
}

impl TransformOrigin {
    /// Returns the initial specified value for `transform-origin`.
    #[inline]
    pub fn initial_value() -> Self {
        Self::new(
            OriginComponent::Length(LengthPercentage::Percentage(ComputedPercentage(0.5))),
            OriginComponent::Length(LengthPercentage::Percentage(ComputedPercentage(0.5))),
            Length::zero(),
        )
    }

    /// Returns the `0 0` value.
    pub fn zero_zero() -> Self {
        Self::new(
            OriginComponent::Length(LengthPercentage::zero()),
            OriginComponent::Length(LengthPercentage::zero()),
            Length::zero(),
        )
    }
}

/// Whether to allow unitless values for perspective in prefixed transform properties.
///
/// See: https://github.com/whatwg/compat/issues/100
#[allow(missing_docs)]
pub enum AllowUnitlessPerspective {
    No,
    Yes,
}

impl Transform {
    /// Parse the transform property value, allowing unitless perspective values.
    ///
    /// This is used for `-webkit-transform` which allows unitless values for perspective.
    #[inline]
    pub(crate) fn parse_legacy<'i, 't>(
        context: &ParserContext,
        input: &mut Parser<'i, 't>,
    ) -> Result<Self, ParseError<'i>> {
        Self::parse_internal(context, input, AllowUnitlessPerspective::Yes)
    }
    /// Internal parse function for deciding if we wish to accept prefixed values or not
    ///
    /// `transform` allows unitless zero angles as an exception, see:
    /// https://github.com/w3c/csswg-drafts/issues/1162
    fn parse_internal<'i, 't>(
        context: &ParserContext,
        input: &mut Parser<'i, 't>,
        allow_unitless_perspective: AllowUnitlessPerspective,
    ) -> Result<Self, ParseError<'i>> {
        use style_traits::{Separator, Space};

        if input
            .try_parse(|input| input.expect_ident_matching("none"))
            .is_ok()
        {
            return Ok(generic::Transform::none());
        }

        Ok(generic::Transform(
            Space::parse(input, |input| {
                let function = input.expect_function()?.clone();
                input.parse_nested_block(|input| {
                    let location = input.current_source_location();
                    let result = match_ignore_ascii_case! { &function,
                        "matrix" => {
                            let a = Number::parse(context, input)?;
                            input.expect_comma()?;
                            let b = Number::parse(context, input)?;
                            input.expect_comma()?;
                            let c = Number::parse(context, input)?;
                            input.expect_comma()?;
                            let d = Number::parse(context, input)?;
                            input.expect_comma()?;
                            // Standard matrix parsing.
                            let e = Number::parse(context, input)?;
                            input.expect_comma()?;
                            let f = Number::parse(context, input)?;
                            Ok(generic::TransformOperation::Matrix(Matrix { a, b, c, d, e, f }))
                        },
                        "matrix3d" => {
                            let m11 = Number::parse(context, input)?;
                            input.expect_comma()?;
                            let m12 = Number::parse(context, input)?;
                            input.expect_comma()?;
                            let m13 = Number::parse(context, input)?;
                            input.expect_comma()?;
                            let m14 = Number::parse(context, input)?;
                            input.expect_comma()?;
                            let m21 = Number::parse(context, input)?;
                            input.expect_comma()?;
                            let m22 = Number::parse(context, input)?;
                            input.expect_comma()?;
                            let m23 = Number::parse(context, input)?;
                            input.expect_comma()?;
                            let m24 = Number::parse(context, input)?;
                            input.expect_comma()?;
                            let m31 = Number::parse(context, input)?;
                            input.expect_comma()?;
                            let m32 = Number::parse(context, input)?;
                            input.expect_comma()?;
                            let m33 = Number::parse(context, input)?;
                            input.expect_comma()?;
                            let m34 = Number::parse(context, input)?;
                            input.expect_comma()?;
                            // Standard matrix3d parsing.
                            let m41 = Number::parse(context, input)?;
                            input.expect_comma()?;
                            let m42 = Number::parse(context, input)?;
                            input.expect_comma()?;
                            let m43 = Number::parse(context, input)?;
                            input.expect_comma()?;
                            let m44 = Number::parse(context, input)?;
                            Ok(generic::TransformOperation::Matrix3D(Matrix3D {
                                m11, m12, m13, m14,
                                m21, m22, m23, m24,
                                m31, m32, m33, m34,
                                m41, m42, m43, m44,
                            }))
                        },
                        "translate" => {
                            let sx = specified::LengthPercentage::parse(context, input)?;
                            if input.try_parse(|input| input.expect_comma()).is_ok() {
                                let sy = specified::LengthPercentage::parse(context, input)?;
                                Ok(generic::TransformOperation::Translate(sx, sy))
                            } else {
                                Ok(generic::TransformOperation::Translate(sx, Zero::zero()))
                            }
                        },
                        "translatex" => {
                            let tx = specified::LengthPercentage::parse(context, input)?;
                            Ok(generic::TransformOperation::TranslateX(tx))
                        },
                        "translatey" => {
                            let ty = specified::LengthPercentage::parse(context, input)?;
                            Ok(generic::TransformOperation::TranslateY(ty))
                        },
                        "translatez" => {
                            let tz = specified::Length::parse(context, input)?;
                            Ok(generic::TransformOperation::TranslateZ(tz))
                        },
                        "translate3d" => {
                            let tx = specified::LengthPercentage::parse(context, input)?;
                            input.expect_comma()?;
                            let ty = specified::LengthPercentage::parse(context, input)?;
                            input.expect_comma()?;
                            let tz = specified::Length::parse(context, input)?;
                            Ok(generic::TransformOperation::Translate3D(tx, ty, tz))
                        },
                        "scale" => {
                            let sx = NumberOrPercentage::parse(context, input)?.to_number();
                            if input.try_parse(|input| input.expect_comma()).is_ok() {
                                let sy = NumberOrPercentage::parse(context, input)?.to_number();
                                Ok(generic::TransformOperation::Scale(sx, sy))
                            } else {
                                Ok(generic::TransformOperation::Scale(sx, sx))
                            }
                        },
                        "scalex" => {
                            let sx = NumberOrPercentage::parse(context, input)?.to_number();
                            Ok(generic::TransformOperation::ScaleX(sx))
                        },
                        "scaley" => {
                            let sy = NumberOrPercentage::parse(context, input)?.to_number();
                            Ok(generic::TransformOperation::ScaleY(sy))
                        },
                        "scalez" => {
                            let sz = NumberOrPercentage::parse(context, input)?.to_number();
                            Ok(generic::TransformOperation::ScaleZ(sz))
                        },
                        "scale3d" => {
                            let sx = NumberOrPercentage::parse(context, input)?.to_number();
                            input.expect_comma()?;
                            let sy = NumberOrPercentage::parse(context, input)?.to_number();
                            input.expect_comma()?;
                            let sz = NumberOrPercentage::parse(context, input)?.to_number();
                            Ok(generic::TransformOperation::Scale3D(sx, sy, sz))
                        },
                        "rotate" => {
                            let theta = specified::Angle::parse_with_unitless(context, input)?;
                            Ok(generic::TransformOperation::Rotate(theta))
                        },
                        "rotatex" => {
                            let theta = specified::Angle::parse_with_unitless(context, input)?;
                            Ok(generic::TransformOperation::RotateX(theta))
                        },
                        "rotatey" => {
                            let theta = specified::Angle::parse_with_unitless(context, input)?;
                            Ok(generic::TransformOperation::RotateY(theta))
                        },
                        "rotatez" => {
                            let theta = specified::Angle::parse_with_unitless(context, input)?;
                            Ok(generic::TransformOperation::RotateZ(theta))
                        },
                        "rotate3d" => {
                            let ax = Number::parse(context, input)?;
                            input.expect_comma()?;
                            let ay = Number::parse(context, input)?;
                            input.expect_comma()?;
                            let az = Number::parse(context, input)?;
                            input.expect_comma()?;
                            let theta = specified::Angle::parse_with_unitless(context, input)?;
                            // TODO(gw): Check that the axis can be normalized.
                            Ok(generic::TransformOperation::Rotate3D(ax, ay, az, theta))
                        },
                        "skew" => {
                            let ax = specified::Angle::parse_with_unitless(context, input)?;
                            if input.try_parse(|input| input.expect_comma()).is_ok() {
                                let ay = specified::Angle::parse_with_unitless(context, input)?;
                                Ok(generic::TransformOperation::Skew(ax, ay))
                            } else {
                                Ok(generic::TransformOperation::Skew(ax, Zero::zero()))
                            }
                        },
                        "skewx" => {
                            let theta = specified::Angle::parse_with_unitless(context, input)?;
                            Ok(generic::TransformOperation::SkewX(theta))
                        },
                        "skewy" => {
                            let theta = specified::Angle::parse_with_unitless(context, input)?;
                            Ok(generic::TransformOperation::SkewY(theta))
                        },
                        "perspective" => {
                            let p = match input.try_parse(|input| {
                                if matches!(allow_unitless_perspective, AllowUnitlessPerspective::Yes) {
                                    specified::Length::parse_non_negative_quirky(context, input, AllowQuirks::Always)
                                } else {
                                    specified::Length::parse_non_negative(context, input)
                                }
                            }) {
                                Ok(p) => generic::PerspectiveFunction::Length(p),
                                Err(..) => {
                                    input.expect_ident_matching("none")?;
                                    generic::PerspectiveFunction::None
                                }
                            };
                            Ok(generic::TransformOperation::Perspective(p))
                        },
                        _ => Err(()),
                    };
                    result.map_err(|()| {
                        location.new_custom_error(StyleParseErrorKind::UnexpectedFunction(
                            function.clone(),
                        ))
                    })
                })
            })?
            .into(),
        ))
    }
}

impl Parse for Transform {
    fn parse<'i, 't>(
        context: &ParserContext,
        input: &mut Parser<'i, 't>,
    ) -> Result<Self, ParseError<'i>> {
        Transform::parse_internal(context, input, AllowUnitlessPerspective::No)
    }
}

/// The specified value of a component of a CSS `<transform-origin>`.
#[derive(Clone, Debug, MallocSizeOf, Parse, PartialEq, SpecifiedValueInfo, ToCss, ToShmem)]
pub enum OriginComponent<S> {
    /// `center`
    Center,
    /// `<length-percentage>`
    Length(LengthPercentage),
    /// `<side>`
    Side(S),
}

impl Parse for TransformOrigin {
    fn parse<'i, 't>(
        context: &ParserContext,
        input: &mut Parser<'i, 't>,
    ) -> Result<Self, ParseError<'i>> {
        let parse_depth = |input: &mut Parser| {
            input
                .try_parse(|i| Length::parse(context, i))
                .unwrap_or(Length::zero())
        };
        match input.try_parse(|i| OriginComponent::parse(context, i)) {
            Ok(x_origin @ OriginComponent::Center) => {
                if let Ok(y_origin) = input.try_parse(|i| OriginComponent::parse(context, i)) {
                    let depth = parse_depth(input);
                    return Ok(Self::new(x_origin, y_origin, depth));
                }
                let y_origin = OriginComponent::Center;
                if let Ok(x_keyword) = input.try_parse(HorizontalPositionKeyword::parse) {
                    let x_origin = OriginComponent::Side(x_keyword);
                    let depth = parse_depth(input);
                    return Ok(Self::new(x_origin, y_origin, depth));
                }
                let depth = Length::from_px(0.);
                return Ok(Self::new(x_origin, y_origin, depth));
            },
            Ok(x_origin) => {
                if let Ok(y_origin) = input.try_parse(|i| OriginComponent::parse(context, i)) {
                    let depth = parse_depth(input);
                    return Ok(Self::new(x_origin, y_origin, depth));
                }
                let y_origin = OriginComponent::Center;
                let depth = Length::from_px(0.);
                return Ok(Self::new(x_origin, y_origin, depth));
            },
            Err(_) => {},
        }
        let y_keyword = VerticalPositionKeyword::parse(input)?;
        let y_origin = OriginComponent::Side(y_keyword);
        if let Ok(x_keyword) = input.try_parse(HorizontalPositionKeyword::parse) {
            let x_origin = OriginComponent::Side(x_keyword);
            let depth = parse_depth(input);
            return Ok(Self::new(x_origin, y_origin, depth));
        }
        if input
            .try_parse(|i| i.expect_ident_matching("center"))
            .is_ok()
        {
            let x_origin = OriginComponent::Center;
            let depth = parse_depth(input);
            return Ok(Self::new(x_origin, y_origin, depth));
        }
        let x_origin = OriginComponent::Center;
        let depth = Length::from_px(0.);
        Ok(Self::new(x_origin, y_origin, depth))
    }
}

impl<S> ToComputedValue for OriginComponent<S>
where
    S: Side,
{
    type ComputedValue = ComputedLengthPercentage;

    fn to_computed_value(&self, context: &Context) -> Self::ComputedValue {
        match *self {
            OriginComponent::Center => {
                ComputedLengthPercentage::new_percent(ComputedPercentage(0.5))
            },
            OriginComponent::Length(ref length) => length.to_computed_value(context),
            OriginComponent::Side(ref keyword) => {
                let p = ComputedPercentage(if keyword.is_start() { 0. } else { 1. });
                ComputedLengthPercentage::new_percent(p)
            },
        }
    }

    fn from_computed_value(computed: &Self::ComputedValue) -> Self {
        OriginComponent::Length(ToComputedValue::from_computed_value(computed))
    }
}

impl<S> OriginComponent<S> {
    /// `0%`
    pub fn zero() -> Self {
        OriginComponent::Length(LengthPercentage::Percentage(ComputedPercentage::zero()))
    }
}

/// A specified CSS `rotate`
pub type Rotate = generic::Rotate<Number, Angle>;

impl Parse for Rotate {
    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(generic::Rotate::None);
        }

        // Parse <angle> or [ x | y | z | <number>{3} ] && <angle>.
        //
        // The rotate axis and angle could be in any order, so we parse angle twice to cover
        // two cases. i.e. `<number>{3} <angle>` or `<angle> <number>{3}`
        let angle = input
            .try_parse(|i| specified::Angle::parse(context, i))
            .ok();
        let axis = input
            .try_parse(|i| {
                Ok(try_match_ident_ignore_ascii_case! { i,
                    "x" => (Number::new(1.), Number::new(0.), Number::new(0.)),
                    "y" => (Number::new(0.), Number::new(1.), Number::new(0.)),
                    "z" => (Number::new(0.), Number::new(0.), Number::new(1.)),
                })
            })
            .or_else(|_: ParseError| -> Result<_, ParseError> {
                input.try_parse(|i| {
                    Ok((
                        Number::parse(context, i)?,
                        Number::parse(context, i)?,
                        Number::parse(context, i)?,
                    ))
                })
            })
            .ok();
        let angle = match angle {
            Some(a) => a,
            None => specified::Angle::parse(context, input)?,
        };

        Ok(match axis {
            Some((x, y, z)) => generic::Rotate::Rotate3D(x, y, z, angle),
            None => generic::Rotate::Rotate(angle),
        })
    }
}

/// A specified CSS `translate`
pub type Translate = generic::Translate<LengthPercentage, Length>;

impl Parse for Translate {
    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(generic::Translate::None);
        }

        let tx = specified::LengthPercentage::parse(context, input)?;
        if let Ok(ty) = input.try_parse(|i| specified::LengthPercentage::parse(context, i)) {
            if let Ok(tz) = input.try_parse(|i| specified::Length::parse(context, i)) {
                // 'translate: <length-percentage> <length-percentage> <length>'
                return Ok(generic::Translate::Translate(tx, ty, tz));
            }

            // translate: <length-percentage> <length-percentage>'
            return Ok(generic::Translate::Translate(
                tx,
                ty,
                specified::Length::zero(),
            ));
        }

        // 'translate: <length-percentage> '
        Ok(generic::Translate::Translate(
            tx,
            specified::LengthPercentage::zero(),
            specified::Length::zero(),
        ))
    }
}

/// A specified CSS `scale`
pub type Scale = generic::Scale<Number>;

impl Parse for Scale {
    /// Scale accepts <number> | <percentage>, so we parse it as NumberOrPercentage,
    /// and then convert into an Number if it's a Percentage.
    /// https://github.com/w3c/csswg-drafts/pull/4396
    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(generic::Scale::None);
        }

        let sx = NumberOrPercentage::parse(context, input)?.to_number();
        if let Ok(sy) = input.try_parse(|i| NumberOrPercentage::parse(context, i)) {
            let sy = sy.to_number();
            if let Ok(sz) = input.try_parse(|i| NumberOrPercentage::parse(context, i)) {
                // 'scale: <number> <number> <number>'
                return Ok(generic::Scale::Scale(sx, sy, sz.to_number()));
            }

            // 'scale: <number> <number>'
            return Ok(generic::Scale::Scale(sx, sy, Number::new(1.0)));
        }

        // 'scale: <number>'
        Ok(generic::Scale::Scale(sx, sx, Number::new(1.0)))
    }
}
