//! CSS properties related to transitions.

use super::{Property, PropertyId};
use crate::compat;
use crate::context::PropertyHandlerContext;
use crate::declaration::{DeclarationBlock, DeclarationList};
use crate::error::{ParserError, PrinterError};
use crate::macros::define_list_shorthand;
use crate::prefixes::Feature;
use crate::printer::Printer;
use crate::properties::masking::get_webkit_mask_property;
use crate::traits::{Parse, PropertyHandler, Shorthand, ToCss, Zero};
use crate::values::ident::CustomIdent;
use crate::values::{easing::EasingFunction, time::Time};
use crate::vendor_prefix::VendorPrefix;
#[cfg(feature = "visitor")]
use crate::visitor::Visit;
use cssparser::*;
use itertools::izip;
use smallvec::SmallVec;

define_list_shorthand! {
  /// A value for the [transition](https://www.w3.org/TR/2018/WD-css-transitions-1-20181011/#transition-shorthand-property) property.
  pub struct Transition<'i>(VendorPrefix) {
    /// The property to transition.
    #[cfg_attr(feature = "serde", serde(borrow))]
    property: TransitionProperty(PropertyId<'i>, VendorPrefix),
    /// The duration of the transition.
    duration: TransitionDuration(Time, VendorPrefix),
    /// The delay before the transition starts.
    delay: TransitionDelay(Time, VendorPrefix),
    /// The easing function for the transition.
    timing_function: TransitionTimingFunction(EasingFunction, VendorPrefix),
  }
}

impl<'i> Parse<'i> for Transition<'i> {
  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
    let mut property = None;
    let mut duration = None;
    let mut delay = None;
    let mut timing_function = None;

    loop {
      if duration.is_none() {
        if let Ok(value) = input.try_parse(Time::parse) {
          duration = Some(value);
          continue;
        }
      }

      if timing_function.is_none() {
        if let Ok(value) = input.try_parse(EasingFunction::parse) {
          timing_function = Some(value);
          continue;
        }
      }

      if delay.is_none() {
        if let Ok(value) = input.try_parse(Time::parse) {
          delay = Some(value);
          continue;
        }
      }

      if property.is_none() {
        if let Ok(value) = input.try_parse(PropertyId::parse) {
          property = Some(value);
          continue;
        }
      }

      break;
    }

    Ok(Transition {
      property: property.unwrap_or(PropertyId::All),
      duration: duration.unwrap_or(Time::Seconds(0.0)),
      delay: delay.unwrap_or(Time::Seconds(0.0)),
      timing_function: timing_function.unwrap_or(EasingFunction::Ease),
    })
  }
}

impl<'i> ToCss for Transition<'i> {
  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
  where
    W: std::fmt::Write,
  {
    self.property.to_css(dest)?;
    if !self.duration.is_zero() || !self.delay.is_zero() {
      dest.write_char(' ')?;
      self.duration.to_css(dest)?;
    }

    if !self.timing_function.is_ease() {
      dest.write_char(' ')?;
      self.timing_function.to_css(dest)?;
    }

    if !self.delay.is_zero() {
      dest.write_char(' ')?;
      self.delay.to_css(dest)?;
    }

    Ok(())
  }
}

/// A value for the [view-transition-name](https://drafts.csswg.org/css-view-transitions-1/#view-transition-name-prop) property.
#[derive(Debug, Clone, PartialEq, Default, Parse, ToCss)]
#[cfg_attr(feature = "visitor", derive(Visit))]
#[cfg_attr(
  feature = "serde",
  derive(serde::Serialize, serde::Deserialize),
  serde(rename_all = "kebab-case")
)]
#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
pub enum ViewTransitionName<'i> {
  /// The element will not participate independently in a view transition.
  #[default]
  None,
  /// The `auto` keyword.
  Auto,
  /// A custom name.
  #[cfg_attr(feature = "serde", serde(borrow, untagged))]
  Custom(CustomIdent<'i>),
}

/// A value for the [view-transition-group](https://drafts.csswg.org/css-view-transitions-2/#view-transition-group-prop) property.
#[derive(Debug, Clone, PartialEq, Default, Parse, ToCss)]
#[cfg_attr(feature = "visitor", derive(Visit))]
#[cfg_attr(
  feature = "serde",
  derive(serde::Serialize, serde::Deserialize),
  serde(rename_all = "kebab-case")
)]
#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
pub enum ViewTransitionGroup<'i> {
  /// The `normal` keyword.
  #[default]
  Normal,
  /// The `contain` keyword.
  Contain,
  /// The `nearest` keyword.
  Nearest,
  /// A custom group.
  #[cfg_attr(feature = "serde", serde(borrow, untagged))]
  Custom(CustomIdent<'i>),
}

#[derive(Default)]
pub(crate) struct TransitionHandler<'i> {
  properties: Option<(SmallVec<[PropertyId<'i>; 1]>, VendorPrefix)>,
  durations: Option<(SmallVec<[Time; 1]>, VendorPrefix)>,
  delays: Option<(SmallVec<[Time; 1]>, VendorPrefix)>,
  timing_functions: Option<(SmallVec<[EasingFunction; 1]>, VendorPrefix)>,
  has_any: bool,
}

impl<'i> PropertyHandler<'i> for TransitionHandler<'i> {
  fn handle_property(
    &mut self,
    property: &Property<'i>,
    dest: &mut DeclarationList<'i>,
    context: &mut PropertyHandlerContext<'i, '_>,
  ) -> bool {
    use Property::*;

    macro_rules! maybe_flush {
      ($prop: ident, $val: expr, $vp: ident) => {{
        // If two vendor prefixes for the same property have different
        // values, we need to flush what we have immediately to preserve order.
        if let Some((val, prefixes)) = &self.$prop {
          if val != $val && !prefixes.contains(*$vp) {
            self.flush(dest, context);
          }
        }
      }};
    }

    macro_rules! property {
      ($feature: ident, $prop: ident, $val: expr, $vp: ident) => {{
        maybe_flush!($prop, $val, $vp);

        // Otherwise, update the value and add the prefix.
        if let Some((val, prefixes)) = &mut self.$prop {
          *val = $val.clone();
          *prefixes |= *$vp;
          *prefixes = context.targets.prefixes(*prefixes, Feature::$feature);
        } else {
          let prefixes = context.targets.prefixes(*$vp, Feature::$feature);
          self.$prop = Some(($val.clone(), prefixes));
          self.has_any = true;
        }
      }};
    }

    match property {
      TransitionProperty(val, vp) => {
        let merged_values = merge_properties(val.iter());
        property!(TransitionProperty, properties, &merged_values, vp);
      }
      TransitionDuration(val, vp) => property!(TransitionDuration, durations, val, vp),
      TransitionDelay(val, vp) => property!(TransitionDelay, delays, val, vp),
      TransitionTimingFunction(val, vp) => property!(TransitionTimingFunction, timing_functions, val, vp),
      Transition(val, vp) => {
        let properties: SmallVec<[PropertyId; 1]> = merge_properties(val.iter().map(|b| &b.property));
        maybe_flush!(properties, &properties, vp);

        let durations: SmallVec<[Time; 1]> = val.iter().map(|b| b.duration.clone()).collect();
        maybe_flush!(durations, &durations, vp);

        let delays: SmallVec<[Time; 1]> = val.iter().map(|b| b.delay.clone()).collect();
        maybe_flush!(delays, &delays, vp);

        let timing_functions: SmallVec<[EasingFunction; 1]> =
          val.iter().map(|b| b.timing_function.clone()).collect();
        maybe_flush!(timing_functions, &timing_functions, vp);

        property!(TransitionProperty, properties, &properties, vp);
        property!(TransitionDuration, durations, &durations, vp);
        property!(TransitionDelay, delays, &delays, vp);
        property!(TransitionTimingFunction, timing_functions, &timing_functions, vp);
      }
      Unparsed(val) if is_transition_property(&val.property_id) => {
        self.flush(dest, context);
        dest.push(Property::Unparsed(
          val.get_prefixed(context.targets, Feature::Transition),
        ));
      }
      _ => return false,
    }

    true
  }

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

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

    self.has_any = false;

    let mut properties = std::mem::take(&mut self.properties);
    let mut durations = std::mem::take(&mut self.durations);
    let mut delays = std::mem::take(&mut self.delays);
    let mut timing_functions = std::mem::take(&mut self.timing_functions);

    let rtl_properties = if let Some((properties, _)) = &mut properties {
      expand_properties(properties, context)
    } else {
      None
    };

    if let (
      Some((properties, property_prefixes)),
      Some((durations, duration_prefixes)),
      Some((delays, delay_prefixes)),
      Some((timing_functions, timing_prefixes)),
    ) = (&mut properties, &mut durations, &mut delays, &mut timing_functions)
    {
      // Find the intersection of prefixes with the same value.
      // Remove that from the prefixes of each of the properties. The remaining
      // prefixes will be handled by outputting individual properties below.
      let intersection = *property_prefixes & *duration_prefixes & *delay_prefixes & *timing_prefixes;
      if !intersection.is_empty() {
        macro_rules! get_transitions {
          ($properties: ident) => {{
            // transition-property determines the number of transitions. The values of other
            // properties are repeated to match this length.
            let mut transitions = SmallVec::with_capacity($properties.len());
            let mut durations_iter = durations.iter().cycle().cloned();
            let mut delays_iter = delays.iter().cycle().cloned();
            let mut timing_iter = timing_functions.iter().cycle().cloned();
            for property_id in $properties {
              let duration = durations_iter.next().unwrap_or(Time::Seconds(0.0));
              let delay = delays_iter.next().unwrap_or(Time::Seconds(0.0));
              let timing_function = timing_iter.next().unwrap_or(EasingFunction::Ease);
              let transition = Transition {
                property: property_id.clone(),
                duration,
                delay,
                timing_function,
              };

              // Expand vendor prefixes into multiple transitions.
              for p in property_id.prefix().or_none() {
                let mut t = transition.clone();
                t.property = property_id.with_prefix(p);
                transitions.push(t);
              }
            }
            transitions
          }};
        }

        let transitions: SmallVec<[Transition; 1]> = get_transitions!(properties);

        if let Some(rtl_properties) = &rtl_properties {
          let rtl_transitions = get_transitions!(rtl_properties);
          context.add_logical_rule(
            Property::Transition(transitions, intersection),
            Property::Transition(rtl_transitions, intersection),
          );
        } else {
          dest.push(Property::Transition(transitions.clone(), intersection));
        }

        property_prefixes.remove(intersection);
        duration_prefixes.remove(intersection);
        delay_prefixes.remove(intersection);
        timing_prefixes.remove(intersection);
      }
    }

    if let Some((properties, prefix)) = properties {
      if !prefix.is_empty() {
        if let Some(rtl_properties) = rtl_properties {
          context.add_logical_rule(
            Property::TransitionProperty(properties, prefix),
            Property::TransitionProperty(rtl_properties, prefix),
          );
        } else {
          dest.push(Property::TransitionProperty(properties, prefix));
        }
      }
    }

    if let Some((durations, prefix)) = durations {
      if !prefix.is_empty() {
        dest.push(Property::TransitionDuration(durations, prefix));
      }
    }

    if let Some((delays, prefix)) = delays {
      if !prefix.is_empty() {
        dest.push(Property::TransitionDelay(delays, prefix));
      }
    }

    if let Some((timing_functions, prefix)) = timing_functions {
      if !prefix.is_empty() {
        dest.push(Property::TransitionTimingFunction(timing_functions, prefix));
      }
    }

    self.reset();
  }

  fn reset(&mut self) {
    self.properties = None;
    self.durations = None;
    self.delays = None;
    self.timing_functions = None;
  }
}

#[inline]
fn is_transition_property(property_id: &PropertyId) -> bool {
  match property_id {
    PropertyId::TransitionProperty(_)
    | PropertyId::TransitionDuration(_)
    | PropertyId::TransitionDelay(_)
    | PropertyId::TransitionTimingFunction(_)
    | PropertyId::Transition(_) => true,
    _ => false,
  }
}

fn merge_properties<'i: 'a, 'a>(val: impl Iterator<Item = &'a PropertyId<'i>>) -> SmallVec<[PropertyId<'i>; 1]> {
  let mut merged_values = SmallVec::<[PropertyId<'_>; 1]>::with_capacity(val.size_hint().1.unwrap_or(1));
  for p in val {
    let without_prefix = p.with_prefix(VendorPrefix::empty());
    if let Some(idx) = merged_values
      .iter()
      .position(|c| c.with_prefix(VendorPrefix::empty()) == without_prefix)
    {
      merged_values[idx].add_prefix(p.prefix());
    } else {
      merged_values.push(p.clone());
    }
  }

  merged_values
}

fn expand_properties<'i>(
  properties: &mut SmallVec<[PropertyId<'i>; 1]>,
  context: &mut PropertyHandlerContext,
) -> Option<SmallVec<[PropertyId<'i>; 1]>> {
  let mut rtl_properties: Option<SmallVec<[PropertyId; 1]>> = None;
  let mut i = 0;

  macro_rules! replace {
    ($properties: ident, $props: ident) => {
      $properties[i] = $props[0].clone();
      if $props.len() > 1 {
        $properties.insert_many(i + 1, $props[1..].into_iter().cloned());
      }
    };
  }

  // Expand logical properties in place.
  while i < properties.len() {
    match get_logical_properties(&properties[i]) {
      LogicalPropertyId::Block(feature, props) if context.should_compile_logical(feature) => {
        replace!(properties, props);
        if let Some(rtl_properties) = &mut rtl_properties {
          replace!(rtl_properties, props);
        }
        i += props.len();
      }
      LogicalPropertyId::Inline(feature, ltr, rtl) if context.should_compile_logical(feature) => {
        // Clone properties to create RTL version only when needed.
        if rtl_properties.is_none() {
          rtl_properties = Some(properties.clone());
        }

        replace!(properties, ltr);
        if let Some(rtl_properties) = &mut rtl_properties {
          replace!(rtl_properties, rtl);
        }

        i += ltr.len();
      }
      _ => {
        // Expand vendor prefixes for targets.
        properties[i].set_prefixes_for_targets(context.targets);

        // Expand mask properties, which use different vendor-prefixed names.
        if let Some(property_id) = get_webkit_mask_property(&properties[i]) {
          if context
            .targets
            .prefixes(VendorPrefix::None, Feature::MaskBorder)
            .contains(VendorPrefix::WebKit)
          {
            properties.insert(i, property_id);
            i += 1;
          }
        }

        if let Some(rtl_properties) = &mut rtl_properties {
          rtl_properties[i].set_prefixes_for_targets(context.targets);

          if let Some(property_id) = get_webkit_mask_property(&rtl_properties[i]) {
            if context
              .targets
              .prefixes(VendorPrefix::None, Feature::MaskBorder)
              .contains(VendorPrefix::WebKit)
            {
              rtl_properties.insert(i, property_id);
            }
          }
        }
        i += 1;
      }
    }
  }

  rtl_properties
}

enum LogicalPropertyId {
  None,
  Block(compat::Feature, &'static [PropertyId<'static>]),
  Inline(
    compat::Feature,
    &'static [PropertyId<'static>],
    &'static [PropertyId<'static>],
  ),
}

#[inline]
fn get_logical_properties(property_id: &PropertyId) -> LogicalPropertyId {
  use compat::Feature::*;
  use LogicalPropertyId::*;
  use PropertyId::*;
  match property_id {
    BlockSize => Block(LogicalSize, &[Height]),
    InlineSize => Inline(LogicalSize, &[Width], &[Height]),
    MinBlockSize => Block(LogicalSize, &[MinHeight]),
    MaxBlockSize => Block(LogicalSize, &[MaxHeight]),
    MinInlineSize => Inline(LogicalSize, &[MinWidth], &[MinHeight]),
    MaxInlineSize => Inline(LogicalSize, &[MaxWidth], &[MaxHeight]),

    InsetBlockStart => Block(LogicalInset, &[Top]),
    InsetBlockEnd => Block(LogicalInset, &[Bottom]),
    InsetInlineStart => Inline(LogicalInset, &[Left], &[Right]),
    InsetInlineEnd => Inline(LogicalInset, &[Right], &[Left]),
    InsetBlock => Block(LogicalInset, &[Top, Bottom]),
    InsetInline => Block(LogicalInset, &[Left, Right]),
    Inset => Block(LogicalInset, &[Top, Bottom, Left, Right]),

    MarginBlockStart => Block(LogicalMargin, &[MarginTop]),
    MarginBlockEnd => Block(LogicalMargin, &[MarginBottom]),
    MarginInlineStart => Inline(LogicalMargin, &[MarginLeft], &[MarginRight]),
    MarginInlineEnd => Inline(LogicalMargin, &[MarginRight], &[MarginLeft]),
    MarginBlock => Block(LogicalMargin, &[MarginTop, MarginBottom]),
    MarginInline => Block(LogicalMargin, &[MarginLeft, MarginRight]),

    PaddingBlockStart => Block(LogicalPadding, &[PaddingTop]),
    PaddingBlockEnd => Block(LogicalPadding, &[PaddingBottom]),
    PaddingInlineStart => Inline(LogicalPadding, &[PaddingLeft], &[PaddingRight]),
    PaddingInlineEnd => Inline(LogicalPadding, &[PaddingRight], &[PaddingLeft]),
    PaddingBlock => Block(LogicalPadding, &[PaddingTop, PaddingBottom]),
    PaddingInline => Block(LogicalPadding, &[PaddingLeft, PaddingRight]),

    BorderBlockStart => Block(LogicalBorders, &[BorderTop]),
    BorderBlockStartWidth => Block(LogicalBorders, &[BorderTopWidth]),
    BorderBlockStartColor => Block(LogicalBorders, &[BorderTopColor]),
    BorderBlockStartStyle => Block(LogicalBorders, &[BorderTopStyle]),

    BorderBlockEnd => Block(LogicalBorders, &[BorderBottom]),
    BorderBlockEndWidth => Block(LogicalBorders, &[BorderBottomWidth]),
    BorderBlockEndColor => Block(LogicalBorders, &[BorderBottomColor]),
    BorderBlockEndStyle => Block(LogicalBorders, &[BorderBottomStyle]),

    BorderInlineStart => Inline(LogicalBorders, &[BorderLeft], &[BorderRight]),
    BorderInlineStartWidth => Inline(LogicalBorders, &[BorderLeftWidth], &[BorderRightWidth]),
    BorderInlineStartColor => Inline(LogicalBorders, &[BorderLeftColor], &[BorderRightColor]),
    BorderInlineStartStyle => Inline(LogicalBorders, &[BorderLeftStyle], &[BorderRightStyle]),

    BorderInlineEnd => Inline(LogicalBorders, &[BorderRight], &[BorderLeft]),
    BorderInlineEndWidth => Inline(LogicalBorders, &[BorderRightWidth], &[BorderLeftWidth]),
    BorderInlineEndColor => Inline(LogicalBorders, &[BorderRightColor], &[BorderLeftColor]),
    BorderInlineEndStyle => Inline(LogicalBorders, &[BorderRightStyle], &[BorderLeftStyle]),

    BorderBlock => Block(LogicalBorders, &[BorderTop, BorderBottom]),
    BorderBlockColor => Block(LogicalBorders, &[BorderTopColor, BorderBottomColor]),
    BorderBlockWidth => Block(LogicalBorders, &[BorderTopWidth, BorderBottomWidth]),
    BorderBlockStyle => Block(LogicalBorders, &[BorderTopStyle, BorderBottomStyle]),

    BorderInline => Block(LogicalBorders, &[BorderLeft, BorderRight]),
    BorderInlineColor => Block(LogicalBorders, &[BorderLeftColor, BorderRightColor]),
    BorderInlineWidth => Block(LogicalBorders, &[BorderLeftWidth, BorderRightWidth]),
    BorderInlineStyle => Block(LogicalBorders, &[BorderLeftStyle, BorderRightStyle]),

    // Not worth using vendor prefixes for these since border-radius is supported
    // everywhere custom properties (which are used to polyfill logical properties) are.
    BorderStartStartRadius => Inline(
      LogicalBorders,
      &[BorderTopLeftRadius(VendorPrefix::None)],
      &[BorderTopRightRadius(VendorPrefix::None)],
    ),
    BorderStartEndRadius => Inline(
      LogicalBorders,
      &[BorderTopRightRadius(VendorPrefix::None)],
      &[BorderTopLeftRadius(VendorPrefix::None)],
    ),
    BorderEndStartRadius => Inline(
      LogicalBorders,
      &[BorderBottomLeftRadius(VendorPrefix::None)],
      &[BorderBottomRightRadius(VendorPrefix::None)],
    ),
    BorderEndEndRadius => Inline(
      LogicalBorders,
      &[BorderBottomRightRadius(VendorPrefix::None)],
      &[BorderBottomLeftRadius(VendorPrefix::None)],
    ),

    _ => None,
  }
}
