///|
pub struct TimelineName {
  css : String
}

///|
pub struct TimelineNames {
  css : String
}

///|
pub struct TimelineScope {
  css : String
}

///|
pub struct AnimationTimeline {
  expression : Expression
}

///|
pub struct AnimationTimelines {
  expression : Expression
}

///|
pub struct TimelineRange {
  expression : Expression
}

///|
pub fn TimelineNames::to_css(self : TimelineNames) -> String {
  self.css
}

///|
pub fn TimelineScope::to_css(self : TimelineScope) -> String {
  self.css
}

///|
pub fn AnimationTimelines::to_css(self : AnimationTimelines) -> String {
  self.expression.to_css()
}

///|
pub fn AnimationTimelines::expression(self : AnimationTimelines) -> Expression {
  self.expression
}

///|
pub fn TimelineRange::to_css(self : TimelineRange) -> String {
  self.expression.to_css()
}

///|
pub fn TimelineRange::expression(self : TimelineRange) -> Expression {
  self.expression
}

///|
pub fn timeline_name(value : String) -> TimelineName {
  if value.has_prefix("--") && value.length() > 2 && valid_grid_name(value) {
    { css: value, }
  } else {
    {
      css: issue_css(
        "STYLE_TIMELINE_NAME", "Timeline names must be dashed identifiers starting with --",
      ),
    }
  }
}

///|
pub fn timeline_names(values : Array[TimelineName]) -> TimelineNames {
  {
    css: if values.is_empty() {
      issue_css("STYLE_TIMELINE_EMPTY", "Timeline names must not be empty")
    } else {
      values.map(value => value.css).join(", ")
    },
  }
}

///|
pub fn timeline_names_none() -> TimelineNames {
  { css: "none", }
}

///|
pub fn timeline_scope(values : Array[TimelineName]) -> TimelineScope {
  { css: timeline_names(values).css, }
}

///|
pub fn timeline_scope_none() -> TimelineScope {
  { css: "none", }
}

///|
pub fn timeline_scope_all() -> TimelineScope {
  { css: "all", }
}

///|
pub fn named_timeline(name : TimelineName) -> AnimationTimeline {
  { expression: literal_expression(name.css, "timeline"), }
}

///|
pub fn timeline_auto() -> AnimationTimeline {
  { expression: literal_expression("auto", "timeline"), }
}

///|
pub fn timeline_none() -> AnimationTimeline {
  { expression: literal_expression("none", "timeline"), }
}

///|
pub(all) enum TimelineAxis {
  BlockAxis
  InlineAxis
  XAxis
  YAxis
} derive(Eq, Debug)

///|
pub extend TimelineAxis with Eq::{equal, not_equal}

///|
pub extend TimelineAxis with @debug.Debug::{to_repr}

///|
pub fn TimelineAxis::to_css(self : TimelineAxis) -> String {
  match self {
    BlockAxis => "block"
    InlineAxis => "inline"
    XAxis => "x"
    YAxis => "y"
  }
}

///|
pub(all) enum TimelineScroller {
  NearestScroller
  RootScroller
  SelfScroller
} derive(Eq, Debug)

///|
pub extend TimelineScroller with Eq::{equal, not_equal}

///|
pub extend TimelineScroller with @debug.Debug::{to_repr}

///|
pub fn scroll_timeline(
  axis~ : TimelineAxis = BlockAxis,
  scroller~ : TimelineScroller = NearestScroller,
) -> AnimationTimeline {
  let scroller = match scroller {
    NearestScroller => "nearest"
    RootScroller => "root"
    SelfScroller => "self"
  }
  {
    expression: literal_expression(
      "scroll(" + scroller + " " + axis.to_css() + ")",
      "timeline",
    ),
  }
}

///|
pub fn view_timeline(
  axis~ : TimelineAxis = BlockAxis,
  inset? : MathExpr[LengthPercentage],
  end_inset? : MathExpr[LengthPercentage],
) -> AnimationTimeline {
  let values = [literal_expression(axis.to_css(), "axis")]
  if inset is Some(value) {
    values.push(value.expression)
  }
  if end_inset is Some(value) {
    if inset is None {
      values.push(literal_expression("auto", "keyword"))
    }
    values.push(value.expression)
  }
  { expression: group_expression("timeline", "view(", values, " ", ")"), }
}

///|
pub fn animation_timelines(
  values : Array[AnimationTimeline],
) -> AnimationTimelines {
  let items = values.map(value => value.expression)
  if items.is_empty() {
    items.push(
      invalid_expression(
        "STYLE_TIMELINE_EMPTY", "Animation timelines must not be empty",
      ),
    )
  }
  { expression: group_expression("timeline-list", "", items, ", ", ""), }
}

///|
pub(all) enum TimelineRangeName {
  Cover
  ContainRange
  Entry
  Exit
  EntryCrossing
  ExitCrossing
} derive(Eq, Debug)

///|
pub extend TimelineRangeName with Eq::{equal, not_equal}

///|
pub extend TimelineRangeName with @debug.Debug::{to_repr}

///|
pub fn timeline_range(
  name : TimelineRangeName,
  offset? : MathExpr[LengthPercentage],
) -> TimelineRange {
  let name = match name {
    Cover => "cover"
    ContainRange => "contain"
    Entry => "entry"
    Exit => "exit"
    EntryCrossing => "entry-crossing"
    ExitCrossing => "exit-crossing"
  }
  let values = [literal_expression(name, "range-name")]
  if offset is Some(value) {
    values.push(value.expression)
  }
  { expression: group_expression("timeline-range", "", values, " ", ""), }
}

///|
pub fn timeline_range_normal() -> TimelineRange {
  { expression: literal_expression("normal", "timeline-range"), }
}

///|
pub fn[T : LengthPercentageMathInput] timeline_range_offset(
  value : T,
) -> TimelineRange {
  { expression: value.length_percentage_math().expression, }
}

///|
pub fn timeline_ranges(values : Array[TimelineRange]) -> TimelineRange {
  let items = values.map(value => value.expression)
  if items.is_empty() {
    items.push(
      invalid_expression(
        "STYLE_TIMELINE_EMPTY", "Timeline ranges must not be empty",
      ),
    )
  }
  { expression: group_expression("timeline-range-list", "", items, ", ", ""), }
}