//! CSS properties related to box sizing.

use crate::compat::Feature;
use crate::context::PropertyHandlerContext;
use crate::declaration::DeclarationList;
use crate::error::{ParserError, PrinterError};
use crate::logical::PropertyCategory;
use crate::macros::{enum_property, property_bitflags};
use crate::printer::Printer;
use crate::properties::{Property, PropertyId};
use crate::traits::{IsCompatible, Parse, PropertyHandler, ToCss};
use crate::values::length::LengthPercentage;
use crate::values::ratio::Ratio;
use crate::vendor_prefix::VendorPrefix;
#[cfg(feature = "visitor")]
use crate::visitor::Visit;
use cssparser::*;

#[cfg(feature = "serde")]
use crate::serialization::*;

// https://drafts.csswg.org/css-sizing-3/#specifying-sizes
// https://www.w3.org/TR/css-sizing-4/#sizing-values

/// A value for the [preferred size properties](https://drafts.csswg.org/css-sizing-3/#preferred-size-properties),
/// i.e. `width` and `height.
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "visitor", derive(Visit))]
#[cfg_attr(
  feature = "serde",
  derive(serde::Serialize, serde::Deserialize),
  serde(tag = "type", rename_all = "kebab-case")
)]
#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
pub enum Size {
  /// The `auto` keyword.
  Auto,
  /// An explicit length or percentage.
  #[cfg_attr(feature = "serde", serde(with = "ValueWrapper::<LengthPercentage>"))]
  LengthPercentage(LengthPercentage),
  /// The `min-content` keyword.
  #[cfg_attr(feature = "serde", serde(with = "PrefixWrapper"))]
  MinContent(VendorPrefix),
  /// The `max-content` keyword.
  #[cfg_attr(feature = "serde", serde(with = "PrefixWrapper"))]
  MaxContent(VendorPrefix),
  /// The `fit-content` keyword.
  #[cfg_attr(feature = "serde", serde(with = "PrefixWrapper"))]
  FitContent(VendorPrefix),
  /// The `fit-content()` function.
  #[cfg_attr(feature = "serde", serde(with = "ValueWrapper::<LengthPercentage>"))]
  FitContentFunction(LengthPercentage),
  /// The `stretch` keyword, or the `-webkit-fill-available` or `-moz-available` prefixed keywords.
  #[cfg_attr(feature = "serde", serde(with = "PrefixWrapper"))]
  Stretch(VendorPrefix),
  /// The `contain` keyword.
  Contain,
}

impl<'i> Parse<'i> for Size {
  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
    let res = input.try_parse(|input| {
      let ident = input.expect_ident()?;
      Ok(match_ignore_ascii_case! { &*ident,
        "auto" => Size::Auto,
        "min-content" => Size::MinContent(VendorPrefix::None),
        "-webkit-min-content" => Size::MinContent(VendorPrefix::WebKit),
        "-moz-min-content" => Size::MinContent(VendorPrefix::Moz),
        "max-content" => Size::MaxContent(VendorPrefix::None),
        "-webkit-max-content" => Size::MaxContent(VendorPrefix::WebKit),
        "-moz-max-content" => Size::MaxContent(VendorPrefix::Moz),
        "stretch" => Size::Stretch(VendorPrefix::None),
        "-webkit-fill-available" => Size::Stretch(VendorPrefix::WebKit),
        "-moz-available" => Size::Stretch(VendorPrefix::Moz),
        "fit-content" => Size::FitContent(VendorPrefix::None),
        "-webkit-fit-content" => Size::FitContent(VendorPrefix::WebKit),
        "-moz-fit-content" => Size::FitContent(VendorPrefix::Moz),
        "contain" => Size::Contain,
        _ => return Err(input.new_custom_error(ParserError::InvalidValue))
      })
    });

    if res.is_ok() {
      return res;
    }

    if let Ok(res) = input.try_parse(parse_fit_content) {
      return Ok(Size::FitContentFunction(res));
    }

    let lp = input.try_parse(LengthPercentage::parse)?;
    Ok(Size::LengthPercentage(lp))
  }
}

impl ToCss for Size {
  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
  where
    W: std::fmt::Write,
  {
    use Size::*;
    match self {
      Auto => dest.write_str("auto"),
      Contain => dest.write_str("contain"),
      MinContent(vp) => {
        vp.to_css(dest)?;
        dest.write_str("min-content")
      }
      MaxContent(vp) => {
        vp.to_css(dest)?;
        dest.write_str("max-content")
      }
      FitContent(vp) => {
        vp.to_css(dest)?;
        dest.write_str("fit-content")
      }
      Stretch(vp) => match *vp {
        VendorPrefix::None => dest.write_str("stretch"),
        VendorPrefix::WebKit => dest.write_str("-webkit-fill-available"),
        VendorPrefix::Moz => dest.write_str("-moz-available"),
        _ => unreachable!(),
      },
      FitContentFunction(l) => {
        dest.write_str("fit-content(")?;
        l.to_css(dest)?;
        dest.write_str(")")
      }
      LengthPercentage(l) => l.to_css(dest),
    }
  }
}

impl IsCompatible for Size {
  fn is_compatible(&self, browsers: crate::targets::Browsers) -> bool {
    use Size::*;
    match self {
      LengthPercentage(l) => l.is_compatible(browsers),
      MinContent(..) => Feature::MinContentSize.is_compatible(browsers),
      MaxContent(..) => Feature::MaxContentSize.is_compatible(browsers),
      FitContent(..) => Feature::FitContentSize.is_compatible(browsers),
      FitContentFunction(l) => {
        Feature::FitContentFunctionSize.is_compatible(browsers) && l.is_compatible(browsers)
      }
      Stretch(vp) => match *vp {
        VendorPrefix::None => Feature::StretchSize,
        VendorPrefix::WebKit => Feature::WebkitFillAvailableSize,
        VendorPrefix::Moz => Feature::MozAvailableSize,
        _ => return false,
      }
      .is_compatible(browsers),
      Contain => false, // ??? no data in mdn
      Auto => true,
    }
  }
}

/// A value for the [minimum](https://drafts.csswg.org/css-sizing-3/#min-size-properties)
/// and [maximum](https://drafts.csswg.org/css-sizing-3/#max-size-properties) size properties,
/// e.g. `min-width` and `max-height`.
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "visitor", derive(Visit))]
#[cfg_attr(
  feature = "serde",
  derive(serde::Serialize, serde::Deserialize),
  serde(tag = "type", rename_all = "kebab-case")
)]
#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
pub enum MaxSize {
  /// The `none` keyword.
  None,
  /// An explicit length or percentage.
  #[cfg_attr(feature = "serde", serde(with = "ValueWrapper::<LengthPercentage>"))]
  LengthPercentage(LengthPercentage),
  /// The `min-content` keyword.
  #[cfg_attr(feature = "serde", serde(with = "PrefixWrapper"))]
  MinContent(VendorPrefix),
  /// The `max-content` keyword.
  #[cfg_attr(feature = "serde", serde(with = "PrefixWrapper"))]
  MaxContent(VendorPrefix),
  /// The `fit-content` keyword.
  #[cfg_attr(feature = "serde", serde(with = "PrefixWrapper"))]
  FitContent(VendorPrefix),
  /// The `fit-content()` function.
  #[cfg_attr(feature = "serde", serde(with = "ValueWrapper::<LengthPercentage>"))]
  FitContentFunction(LengthPercentage),
  /// The `stretch` keyword, or the `-webkit-fill-available` or `-moz-available` prefixed keywords.
  #[cfg_attr(feature = "serde", serde(with = "PrefixWrapper"))]
  Stretch(VendorPrefix),
  /// The `contain` keyword.
  Contain,
}

impl<'i> Parse<'i> for MaxSize {
  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
    let res = input.try_parse(|input| {
      let ident = input.expect_ident()?;
      Ok(match_ignore_ascii_case! { &*ident,
        "none" => MaxSize::None,
        "min-content" => MaxSize::MinContent(VendorPrefix::None),
        "-webkit-min-content" => MaxSize::MinContent(VendorPrefix::WebKit),
        "-moz-min-content" => MaxSize::MinContent(VendorPrefix::Moz),
        "max-content" => MaxSize::MaxContent(VendorPrefix::None),
        "-webkit-max-content" => MaxSize::MaxContent(VendorPrefix::WebKit),
        "-moz-max-content" => MaxSize::MaxContent(VendorPrefix::Moz),
        "stretch" => MaxSize::Stretch(VendorPrefix::None),
        "-webkit-fill-available" => MaxSize::Stretch(VendorPrefix::WebKit),
        "-moz-available" => MaxSize::Stretch(VendorPrefix::Moz),
        "fit-content" => MaxSize::FitContent(VendorPrefix::None),
        "-webkit-fit-content" => MaxSize::FitContent(VendorPrefix::WebKit),
        "-moz-fit-content" => MaxSize::FitContent(VendorPrefix::Moz),
        "contain" => MaxSize::Contain,
        _ => return Err(input.new_custom_error(ParserError::InvalidValue))
      })
    });

    if res.is_ok() {
      return res;
    }

    if let Ok(res) = input.try_parse(parse_fit_content) {
      return Ok(MaxSize::FitContentFunction(res));
    }

    let lp = input.try_parse(LengthPercentage::parse)?;
    Ok(MaxSize::LengthPercentage(lp))
  }
}

impl ToCss for MaxSize {
  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
  where
    W: std::fmt::Write,
  {
    use MaxSize::*;
    match self {
      None => dest.write_str("none"),
      Contain => dest.write_str("contain"),
      MinContent(vp) => {
        vp.to_css(dest)?;
        dest.write_str("min-content")
      }
      MaxContent(vp) => {
        vp.to_css(dest)?;
        dest.write_str("max-content")
      }
      FitContent(vp) => {
        vp.to_css(dest)?;
        dest.write_str("fit-content")
      }
      Stretch(vp) => match *vp {
        VendorPrefix::None => dest.write_str("stretch"),
        VendorPrefix::WebKit => dest.write_str("-webkit-fill-available"),
        VendorPrefix::Moz => dest.write_str("-moz-available"),
        _ => unreachable!(),
      },
      FitContentFunction(l) => {
        dest.write_str("fit-content(")?;
        l.to_css(dest)?;
        dest.write_str(")")
      }
      LengthPercentage(l) => l.to_css(dest),
    }
  }
}

impl IsCompatible for MaxSize {
  fn is_compatible(&self, browsers: crate::targets::Browsers) -> bool {
    use MaxSize::*;
    match self {
      LengthPercentage(l) => l.is_compatible(browsers),
      MinContent(..) => Feature::MinContentSize.is_compatible(browsers),
      MaxContent(..) => Feature::MaxContentSize.is_compatible(browsers),
      FitContent(..) => Feature::FitContentSize.is_compatible(browsers),
      FitContentFunction(l) => {
        Feature::FitContentFunctionSize.is_compatible(browsers) && l.is_compatible(browsers)
      }
      Stretch(vp) => match *vp {
        VendorPrefix::None => Feature::StretchSize,
        VendorPrefix::WebKit => Feature::WebkitFillAvailableSize,
        VendorPrefix::Moz => Feature::MozAvailableSize,
        _ => return false,
      }
      .is_compatible(browsers),
      Contain => false, // ??? no data in mdn
      None => true,
    }
  }
}

fn parse_fit_content<'i, 't>(
  input: &mut Parser<'i, 't>,
) -> Result<LengthPercentage, ParseError<'i, ParserError<'i>>> {
  input.expect_function_matching("fit-content")?;
  input.parse_nested_block(|input| LengthPercentage::parse(input))
}

enum_property! {
  /// A value for the [box-sizing](https://drafts.csswg.org/css-sizing-3/#box-sizing) property.
  pub enum BoxSizing {
    /// Exclude the margin/border/padding from the width and height.
    ContentBox,
    /// Include the padding and border (but not the margin) in the width and height.
    BorderBox,
  }
}

/// A value for the [aspect-ratio](https://drafts.csswg.org/css-sizing-4/#aspect-ratio) property.
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "visitor", derive(Visit))]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
pub struct AspectRatio {
  /// The `auto` keyword.
  pub auto: bool,
  /// A preferred aspect ratio for the box, specified as width / height.
  pub ratio: Option<Ratio>,
}

impl<'i> Parse<'i> for AspectRatio {
  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
    let location = input.current_source_location();
    let mut auto = input.try_parse(|i| i.expect_ident_matching("auto"));
    let ratio = input.try_parse(Ratio::parse);
    if auto.is_err() {
      auto = input.try_parse(|i| i.expect_ident_matching("auto"));
    }
    if auto.is_err() && ratio.is_err() {
      return Err(location.new_custom_error(ParserError::InvalidValue));
    }

    Ok(AspectRatio {
      auto: auto.is_ok(),
      ratio: ratio.ok(),
    })
  }
}

impl ToCss for AspectRatio {
  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
  where
    W: std::fmt::Write,
  {
    if self.auto {
      dest.write_str("auto")?;
    }

    if let Some(ratio) = &self.ratio {
      if self.auto {
        dest.write_char(' ')?;
      }
      ratio.to_css(dest)?;
    }

    Ok(())
  }
}

property_bitflags! {
  #[derive(Default)]
  struct SizeProperty: u16 {
    const Width = 1 << 0;
    const Height = 1 << 1;
    const MinWidth = 1 << 2;
    const MinHeight = 1 << 3;
    const MaxWidth = 1 << 4;
    const MaxHeight = 1 << 5;
    const BlockSize = 1 << 6;
    const InlineSize = 1 << 7;
    const MinBlockSize  = 1 << 8;
    const MinInlineSize = 1 << 9;
    const MaxBlockSize = 1 << 10;
    const MaxInlineSize = 1 << 11;
  }
}

#[derive(Default)]
pub(crate) struct SizeHandler {
  width: Option<Size>,
  height: Option<Size>,
  min_width: Option<Size>,
  min_height: Option<Size>,
  max_width: Option<MaxSize>,
  max_height: Option<MaxSize>,
  block_size: Option<Size>,
  inline_size: Option<Size>,
  min_block_size: Option<Size>,
  min_inline_size: Option<Size>,
  max_block_size: Option<MaxSize>,
  max_inline_size: Option<MaxSize>,
  has_any: bool,
  flushed_properties: SizeProperty,
  category: PropertyCategory,
}

impl<'i> PropertyHandler<'i> for SizeHandler {
  fn handle_property(
    &mut self,
    property: &Property<'i>,
    dest: &mut DeclarationList<'i>,
    context: &mut PropertyHandlerContext<'i, '_>,
  ) -> bool {
    let logical_supported = !context.should_compile_logical(Feature::LogicalSize);

    macro_rules! property {
      ($prop: ident, $val: ident, $category: ident) => {{
        // If the category changes betweet logical and physical,
        // or if the value contains syntax that isn't supported across all targets,
        // preserve the previous value as a fallback.
        if PropertyCategory::$category != self.category || (self.$prop.is_some() && matches!(context.targets.browsers, Some(targets) if !$val.is_compatible(targets))) {
          self.flush(dest, context);
        }

        self.$prop = Some($val.clone());
        self.category = PropertyCategory::$category;
        self.has_any = true;
      }};
    }

    match property {
      Property::Width(v) => property!(width, v, Physical),
      Property::Height(v) => property!(height, v, Physical),
      Property::MinWidth(v) => property!(min_width, v, Physical),
      Property::MinHeight(v) => property!(min_height, v, Physical),
      Property::MaxWidth(v) => property!(max_width, v, Physical),
      Property::MaxHeight(v) => property!(max_height, v, Physical),
      Property::BlockSize(size) => property!(block_size, size, Logical),
      Property::MinBlockSize(size) => property!(min_block_size, size, Logical),
      Property::MaxBlockSize(size) => property!(max_block_size, size, Logical),
      Property::InlineSize(size) => property!(inline_size, size, Logical),
      Property::MinInlineSize(size) => property!(min_inline_size, size, Logical),
      Property::MaxInlineSize(size) => property!(max_inline_size, size, Logical),
      Property::Unparsed(unparsed) => {
        self.flush(dest, context);
        macro_rules! logical_unparsed {
          ($physical: ident) => {
            if logical_supported {
              self
                .flushed_properties
                .insert(SizeProperty::try_from(&unparsed.property_id).unwrap());
              dest.push(property.clone());
            } else {
              dest.push(Property::Unparsed(
                unparsed.with_property_id(PropertyId::$physical),
              ));
              self.flushed_properties.insert(SizeProperty::$physical);
            }
          };
        }

        match &unparsed.property_id {
          PropertyId::Width
          | PropertyId::Height
          | PropertyId::MinWidth
          | PropertyId::MaxWidth
          | PropertyId::MinHeight
          | PropertyId::MaxHeight => {
            self
              .flushed_properties
              .insert(SizeProperty::try_from(&unparsed.property_id).unwrap());
            dest.push(property.clone());
          }
          PropertyId::BlockSize => logical_unparsed!(Height),
          PropertyId::MinBlockSize => logical_unparsed!(MinHeight),
          PropertyId::MaxBlockSize => logical_unparsed!(MaxHeight),
          PropertyId::InlineSize => logical_unparsed!(Width),
          PropertyId::MinInlineSize => logical_unparsed!(MinWidth),
          PropertyId::MaxInlineSize => logical_unparsed!(MaxWidth),
          _ => return false,
        }
      }
      _ => return false,
    }

    true
  }

  fn finalize(&mut self, dest: &mut DeclarationList, context: &mut PropertyHandlerContext<'i, '_>) {
    self.flush(dest, context);
    self.flushed_properties = SizeProperty::empty();
  }
}

impl SizeHandler {
  fn flush<'i>(&mut self, dest: &mut DeclarationList, context: &mut PropertyHandlerContext<'i, '_>) {
    if !self.has_any {
      return;
    }

    self.has_any = false;
    let logical_supported = !context.should_compile_logical(Feature::LogicalSize);

    macro_rules! prefix {
      ($prop: ident, $size: ident, $feature: ident) => {
        if !self.flushed_properties.contains(SizeProperty::$prop) {
          let prefixes =
            context.targets.prefixes(VendorPrefix::None, crate::prefixes::Feature::$feature) - VendorPrefix::None;
          for prefix in prefixes {
            dest.push(Property::$prop($size::$feature(prefix)));
          }
        }
      };
    }

    macro_rules! property {
      ($prop: ident, $val: ident, $size: ident) => {{
        if let Some(val) = std::mem::take(&mut self.$val) {
          match val {
            $size::Stretch(VendorPrefix::None) => prefix!($prop, $size, Stretch),
            $size::MinContent(VendorPrefix::None) => prefix!($prop, $size, MinContent),
            $size::MaxContent(VendorPrefix::None) => prefix!($prop, $size, MaxContent),
            $size::FitContent(VendorPrefix::None) => prefix!($prop, $size, FitContent),
            _ => {}
          }
          dest.push(Property::$prop(val.clone()));
          self.flushed_properties.insert(SizeProperty::$prop);
        }
      }};
    }

    macro_rules! logical {
      ($prop: ident, $val: ident, $physical: ident, $size: ident) => {
        if logical_supported {
          property!($prop, $val, $size);
        } else {
          property!($physical, $val, $size);
        }
      };
    }

    property!(Width, width, Size);
    property!(MinWidth, min_width, Size);
    property!(MaxWidth, max_width, MaxSize);
    property!(Height, height, Size);
    property!(MinHeight, min_height, Size);
    property!(MaxHeight, max_height, MaxSize);
    logical!(BlockSize, block_size, Height, Size);
    logical!(MinBlockSize, min_block_size, MinHeight, Size);
    logical!(MaxBlockSize, max_block_size, MaxHeight, MaxSize);
    logical!(InlineSize, inline_size, Width, Size);
    logical!(MinInlineSize, min_inline_size, MinWidth, Size);
    logical!(MaxInlineSize, max_inline_size, MaxWidth, MaxSize);
  }
}
