///|
fn animation_list_index(length : Int, index : Int) -> Int {
  if length == 0 {
    0
  } else {
    index % length
  }
}

///|
fn animation_duration(style : @css_style.Style, index : Int) -> Double {
  if style.animation_duration.is_empty() {
    0.0
  } else {
    style.animation_duration[animation_list_index(
      style.animation_duration.length(),
      index,
    )]
  }
}

///|
fn animation_delay(style : @css_style.Style, index : Int) -> Double {
  if style.animation_delay.is_empty() {
    0.0
  } else {
    style.animation_delay[animation_list_index(
      style.animation_delay.length(),
      index,
    )]
  }
}

///|
fn animation_easing(
  style : @css_style.Style,
  index : Int,
) -> @css_values.Easing {
  if style.animation_timing_function.is_empty() {
    Ease
  } else {
    style.animation_timing_function[animation_list_index(
      style.animation_timing_function.length(),
      index,
    )]
  }
}

///|
fn animation_iteration_count(
  style : @css_style.Style,
  index : Int,
) -> @css_values.AnimationIterationCount {
  if style.animation_iteration_count.is_empty() {
    Count(1.0)
  } else {
    style.animation_iteration_count[animation_list_index(
      style.animation_iteration_count.length(),
      index,
    )]
  }
}

///|
fn animation_direction(
  style : @css_style.Style,
  index : Int,
) -> @css_values.AnimationDirection {
  if style.animation_direction.is_empty() {
    Normal
  } else {
    style.animation_direction[animation_list_index(
      style.animation_direction.length(),
      index,
    )]
  }
}

///|
fn animation_fill_mode(
  style : @css_style.Style,
  index : Int,
) -> @css_values.AnimationFillMode {
  if style.animation_fill_mode.is_empty() {
    None
  } else {
    style.animation_fill_mode[animation_list_index(
      style.animation_fill_mode.length(),
      index,
    )]
  }
}

///|
fn animation_play_state(
  style : @css_style.Style,
  index : Int,
) -> @css_values.AnimationPlayState {
  if style.animation_play_state.is_empty() {
    Running
  } else {
    style.animation_play_state[animation_list_index(
      style.animation_play_state.length(),
      index,
    )]
  }
}

///|
fn animation_has_backwards_fill(mode : @css_values.AnimationFillMode) -> Bool {
  mode is Backwards || mode is Both
}

///|
fn animation_has_forwards_fill(mode : @css_values.AnimationFillMode) -> Bool {
  mode is Forwards || mode is Both
}

///|
fn directed_animation_progress(
  progress : Double,
  iteration : Int,
  direction : @css_values.AnimationDirection,
) -> Double {
  let reverse = match direction {
    Normal => false
    Reverse => true
    Alternate => iteration % 2 == 1
    AlternateReverse => iteration % 2 == 0
  }
  if reverse {
    1.0 - progress
  } else {
    progress
  }
}

///|
fn sampled_animation_progress(
  style : @css_style.Style,
  index : Int,
  sample_time_seconds : Double,
) -> Double? {
  let duration = animation_duration(style, index)
  let delay = animation_delay(style, index)
  let fill = animation_fill_mode(style, index)
  let direction = animation_direction(style, index)
  let timeline_time = if animation_play_state(style, index) is Paused {
    0.0
  } else {
    sample_time_seconds
  }
  let local_time = timeline_time - delay
  if local_time < 0.0 {
    if animation_has_backwards_fill(fill) {
      return Some(directed_animation_progress(0.0, 0, direction))
    }
    return None
  }
  if duration <= 0.0 {
    if animation_has_forwards_fill(fill) || local_time == 0.0 {
      return Some(directed_animation_progress(1.0, 0, direction))
    }
    return None
  }
  let iteration_count = animation_iteration_count(style, index)
  match iteration_count {
    Count(count) => {
      if count <= 0.0 {
        return None
      }
      let active_duration = duration * count
      if local_time >= active_duration {
        if !animation_has_forwards_fill(fill) {
          return None
        }
        let final_iteration = (count.ceil().to_int() - 1).max(0)
        let fractional = count - count.floor()
        let final_progress = if fractional == 0.0 { 1.0 } else { fractional }
        return Some(
          directed_animation_progress(
            final_progress, final_iteration, direction,
          ),
        )
      }
    }
    Infinite => ()
  }
  let iteration = (local_time / duration).floor().to_int()
  let progress = (local_time - iteration.to_double() * duration) / duration
  Some(directed_animation_progress(progress, iteration, direction))
}

///|
fn find_animation_keyframes(
  stylesheets : Array[@css_cascade.Stylesheet],
  name : String,
) -> @css_cascade.KeyframesRule? {
  let mut found : @css_cascade.KeyframesRule? = None
  for stylesheet in stylesheets {
    match stylesheet.find_keyframes(name) {
      Some(rule) => found = Some(rule)
      None => ()
    }
  }
  found
}

///|
fn animation_length_basis(property : String) -> LengthAxis {
  match property {
    "x" | "width" | "cx" | "x1" | "x2" | "rx" => Horizontal
    "y" | "height" | "cy" | "y1" | "y2" | "ry" => Vertical
    _ => Diagonal
  }
}

///|
fn animation_property_is_length(property : String) -> Bool {
  match property {
    "x"
    | "y"
    | "width"
    | "height"
    | "cx"
    | "cy"
    | "r"
    | "rx"
    | "ry"
    | "x1"
    | "y1"
    | "x2"
    | "y2"
    | "stroke-width"
    | "stroke-dashoffset" => true
    _ => false
  }
}

///|
fn animation_property_is_number(property : String) -> Bool {
  match property {
    "opacity"
    | "fill-opacity"
    | "stroke-opacity"
    | "stop-opacity"
    | "stroke-miterlimit" => true
    _ => false
  }
}

///|
fn animation_property_is_color(property : String) -> Bool {
  match property {
    "fill" | "stroke" | "color" | "stop-color" => true
    _ => false
  }
}

///|
fn interpolate_animation_color(
  from : String,
  to : String,
  progress : Double,
) -> String? {
  match (@css_computed.parse_color(from), @css_computed.parse_color(to)) {
    (Resolved(left), Resolved(right)) => {
      let r = (left.r.to_double() + (right.r - left.r).to_double() * progress)
        .round()
        .to_int()
      let g = (left.g.to_double() + (right.g - left.g).to_double() * progress)
        .round()
        .to_int()
      let b = (left.b.to_double() + (right.b - left.b).to_double() * progress)
        .round()
        .to_int()
      let a = left.a + (right.a - left.a) * progress
      Some("rgba(\{r}, \{g}, \{b}, \{a})")
    }
    _ => None
  }
}

///|
fn animation_color_text(color : Color) -> String {
  "rgba(\{color.r}, \{color.g}, \{color.b}, \{color.a.to_double() / 255.0})"
}

///|
fn animation_paint_text(paint : Paint) -> String {
  match paint {
    None => "none"
    SolidColor(color) => animation_color_text(color)
    CurrentColor => "currentColor"
    PaintServerRef(id, _) => "url(#\{id})"
    LinearGrad(_) | RadialGrad(_) => "none"
  }
}

///|
fn underlying_animation_value(
  property : String,
  base_declarations : Array[StyleDeclaration],
  attrs : Array[(String, String)],
  custom_properties : Map[String, String],
  parent : InheritedStyle,
  css_style : @css_style.Style,
) -> String? {
  match find_winning_declaration(base_declarations, property) {
    Some(declaration) =>
      match
        @css.resolve_custom_property_value(declaration.value, custom_properties) {
        Some(value) =>
          match trim_string(value).to_lower() {
            "initial" | "inherit" | "unset" | "revert" | "revert-layer" => ()
            _ => return Some(value)
          }
        None => ()
      }
    None => ()
  }
  match get_attr(attrs, property) {
    Some(value) => return Some(value)
    None => ()
  }
  match property {
    "fill" => Some(animation_paint_text(parent.fill))
    "fill-opacity" => Some(parent.fill_opacity.to_string())
    "stroke" => Some(animation_paint_text(parent.stroke.paint))
    "stroke-width" => Some(parent.stroke.width.to_string())
    "stroke-opacity" => Some(parent.stroke_opacity.to_string())
    "color" => Some(animation_color_text(parent.color))
    "stop-color" => Some(animation_color_text(parent.stop_color))
    "stop-opacity" => Some(parent.stop_opacity.to_string())
    "opacity" => Some(css_style.opacity.to_string())
    "transform" => Some("matrix(1, 0, 0, 1, 0, 0)")
    "x"
    | "y"
    | "width"
    | "height"
    | "cx"
    | "cy"
    | "r"
    | "rx"
    | "ry"
    | "x1"
    | "y1"
    | "x2"
    | "y2"
    | "stroke-dashoffset" => Some("0")
    _ => None
  }
}

///|
fn interpolate_animation_value(
  property : String,
  from : String,
  to : String,
  progress : Double,
  length_context : LengthContext,
) -> String {
  if animation_property_is_number(property) {
    match (parse_number_strict(from), parse_number_strict(to)) {
      (Some(left), Some(right)) =>
        return (left + (right - left) * progress).to_string()
      _ => ()
    }
  } else if animation_property_is_length(property) {
    let basis = animation_length_basis(property)
    match
      (
        parse_length_with_context_optional(from, basis, length_context),
        parse_length_with_context_optional(to, basis, length_context),
      ) {
      (Some(left), Some(right)) =>
        return (left + (right - left) * progress).to_string()
      _ => ()
    }
  } else if animation_property_is_color(property) {
    match interpolate_animation_color(from, to, progress) {
      Some(value) => return value
      None => ()
    }
  } else if property == "transform" {
    let left = parse_transform(from)
    let right = parse_transform(to)
    return "matrix(\{left.a + (right.a - left.a) * progress}, \{left.b + (right.b - left.b) * progress}, \{left.c + (right.c - left.c) * progress}, \{left.d + (right.d - left.d) * progress}, \{left.e + (right.e - left.e) * progress}, \{left.f + (right.f - left.f) * progress})"
  }
  if progress < 0.5 {
    from
  } else {
    to
  }
}

///|
fn sample_keyframe_property(
  frames : Array[(Double, String)],
  progress : Double,
  easing : @css_values.Easing,
  property : String,
  length_context : LengthContext,
) -> String {
  frames.sort_by(fn(left, right) {
    if left.0 < right.0 {
      -1
    } else if left.0 > right.0 {
      1
    } else {
      0
    }
  })
  if frames.length() == 1 || progress <= frames[0].0 {
    return frames[0].1
  }
  let last = frames[frames.length() - 1]
  if progress >= last.0 {
    return last.1
  }
  for i = 0; i + 1 < frames.length(); i = i + 1 {
    let left = frames[i]
    let right = frames[i + 1]
    if progress >= left.0 && progress <= right.0 {
      let span = right.0 - left.0
      let segment_progress = if span <= 0.0 {
        1.0
      } else {
        (progress - left.0) / span
      }
      return interpolate_animation_value(
        property,
        left.1,
        right.1,
        easing.sample(segment_progress),
        length_context,
      )
    }
  }
  last.1
}

///|
fn sampled_animation_declarations(
  style : @css_style.Style,
  stylesheets : Array[@css_cascade.Stylesheet],
  sample_time_seconds : Double,
  custom_properties : Map[String, String],
  length_context : LengthContext,
  base_declarations : Array[StyleDeclaration],
  attrs : Array[(String, String)],
  parent : InheritedStyle,
) -> Array[StyleDeclaration] {
  let sampled : Map[String, String] = Map([])
  for animation_index, name in style.animation_name {
    if name.to_lower() == "none" {
      continue
    }
    let progress = match
      sampled_animation_progress(style, animation_index, sample_time_seconds) {
      Some(progress) => progress
      None => continue
    }
    let keyframes = match find_animation_keyframes(stylesheets, name) {
      Some(keyframes) => keyframes
      None => continue
    }
    let property_frames : Map[String, Array[(Double, String)]] = Map([])
    for block in keyframes.blocks {
      for declaration in block.declarations {
        if declaration.property == "animation-timing-function" ||
          declaration.importance is Important {
          continue
        }
        let value = match declaration.value {
          Value(value) =>
            @css.resolve_custom_property_value(value, custom_properties).unwrap_or(
              value,
            )
          _ => continue
        }
        let frames = property_frames.get(declaration.property).unwrap_or([])
        for offset in block.offsets {
          frames.push((offset, value))
        }
        property_frames[declaration.property] = frames
      }
    }
    let easing = animation_easing(style, animation_index)
    property_frames.each(fn(property, frames) {
      if !frames.is_empty() {
        let mut has_start = false
        let mut has_end = false
        for frame in frames {
          if frame.0 == 0.0 {
            has_start = true
          }
          if frame.0 == 1.0 {
            has_end = true
          }
        }
        if !has_start || !has_end {
          match
            underlying_animation_value(
              property, base_declarations, attrs, custom_properties, parent, style,
            ) {
            Some(value) => {
              if !has_start {
                frames.push((0.0, value))
              }
              if !has_end {
                frames.push((1.0, value))
              }
            }
            None => ()
          }
        }
        sampled[property] = sample_keyframe_property(
          frames, progress, easing, property, length_context,
        )
      }
    })
  }
  let declarations : Array[StyleDeclaration] = []
  sampled.each(fn(name, value) { declarations.push({ name, value }) })
  declarations
}