///|
enum TrackKind {
  Fixed
  Flexible
  Intrinsic
  MinMax(Bool)
  FitContent
  Named
  Repeat(Bool, Bool)
} derive(Eq, Debug)

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

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

///|
fn make_track(expression : Expression, kind : TrackKind) -> GridTrack {
  { css: expression.to_css(), expression, kind, }
}

///|
fn invalid_track(message : String) -> GridTrack {
  make_track(
    literal_expression(issue_css("STYLE_GRID_TRACK", message), "grid-track"),
    Intrinsic,
  )
}

///|
fn GridTrack::is_fixed(self : GridTrack) -> Bool {
  match self.kind {
    Fixed | MinMax(true) | Repeat(true, _) | Named => true
    _ => false
  }
}

///|
fn GridTrack::is_breadth(self : GridTrack) -> Bool {
  match self.kind {
    Fixed | Flexible | Intrinsic => true
    _ => false
  }
}

///|
fn make_tracks(tracks : Array[GridTrack]) -> GridTrackList {
  let values = tracks.map(track => track.expression)
  let mut previous_named = false
  for track in tracks {
    if previous_named && track.kind == Named {
      values.push(
        literal_expression(
          issue_css(
            "STYLE_GRID_TRACK", "Combine names for one grid line in a single named_lines call",
          ),
          "grid-track",
        ),
      )
      break
    }
    previous_named = track.kind == Named
  }
  let auto_count = tracks
    .filter(track => {
      match track.kind {
        Repeat(_, true) => true
        _ => false
      }
    })
    .length()
  if tracks.is_empty() || tracks.every(track => track.kind == Named) {
    values.push(
      literal_expression(
        issue_css("STYLE_GRID_TRACK", "A track list requires a track size"),
        "grid-track",
      ),
    )
  }
  if auto_count > 1 ||
    (auto_count == 1 && !tracks.every(track => track.is_fixed())) {
    values.push(
      literal_expression(
        issue_css(
          "STYLE_GRID_AUTO_REPEAT", "Auto-repeat permits one repetition and requires fixed-size tracks",
        ),
        "grid-track",
      ),
    )
  }
  let expression = group_expression("grid-track-list", "", values, " ", "")
  {
    css: expression.to_css(),
    expression,
    tracks: tracks.copy(),
    subgrid: false,
  }
}

///|
fn make_repeat(
  count : String,
  tracks : GridTrackList,
  automatic : Bool,
) -> GridTrack {
  if tracks.subgrid ||
    tracks.tracks.any(track => {
      match track.kind {
        Repeat(_, _) => true
        _ => false
      }
    }) {
    return invalid_track(
      "Nested repeats and subgrid inside repeat are not supported by the grid grammar",
    )
  }
  if automatic && !tracks.tracks.every(track => track.is_fixed()) {
    return invalid_track("Auto-repeat requires fixed-size tracks")
  }
  make_track(
    group_expression(
      "grid-track",
      "repeat(" + count + ", ",
      [tracks.expression],
      "",
      ")",
    ),
    Repeat(tracks.tracks.every(track => track.is_fixed()), automatic),
  )
}

///|
pub fn auto_fit(tracks : GridTrackList) -> GridTrack {
  make_repeat("auto-fit", tracks, true)
}

///|
pub fn auto_fill(tracks : GridTrackList) -> GridTrack {
  make_repeat("auto-fill", tracks, true)
}

///|
pub fn track_auto() -> GridTrack {
  make_track(literal_expression("auto", "grid-track"), Intrinsic)
}

///|
pub fn track_min_content() -> GridTrack {
  make_track(literal_expression("min-content", "grid-track"), Intrinsic)
}

///|
pub fn track_max_content() -> GridTrack {
  make_track(literal_expression("max-content", "grid-track"), Intrinsic)
}

///|
pub fn[T : LengthPercentageMathInput] fit_content(value : T) -> GridTrack {
  make_track(
    group_expression(
      "grid-track",
      "fit-content(",
      [value.length_percentage_math().expression.constrain("nonnegative")],
      "",
      ")",
    ),
    FitContent,
  )
}

///|
fn grid_name_start(code : Int32) -> Bool {
  (code >= 65 && code <= 90) ||
  (code >= 97 && code <= 122) ||
  code == 95 ||
  code >= 128
}

///|
fn valid_custom_ident(name : String) -> Bool {
  if name.is_empty() {
    return false
  }
  let first = name.unsafe_get(0).to_int()
  if first == 45 {
    if name.length() < 2 {
      return false
    }
    let second = name.unsafe_get(1).to_int()
    if second != 45 && !grid_name_start(second) {
      return false
    }
  } else if !grid_name_start(first) {
    return false
  }
  for index = 1; index < name.length(); index = index + 1 {
    let code = name.unsafe_get(index).to_int()
    if !grid_name_start(code) && code != 45 && !(code >= 48 && code <= 57) {
      return false
    }
  }
  !["initial", "inherit", "unset", "revert", "revert-layer", "default"].contains(
    name.to_lower(),
  )
}

///|
fn valid_grid_name(name : String) -> Bool {
  valid_custom_ident(name) && !["auto", "span"].contains(name.to_lower())
}

///|
// CSS Grid 2 line-names use custom-ident; reserved keywords are ASCII caseless.
// https://www.w3.org/TR/css-grid-2/#named-lines
pub fn named_lines(names : Array[String]) -> GridTrack {
  if names.any(name => !valid_grid_name(name)) {
    return invalid_track("Grid line names must be unescaped custom identifiers")
  }
  make_track(
    literal_expression("[" + names.join(" ") + "]", "grid-track"),
    Named,
  )
}

///|
pub fn subgrid(lines~ : Array[Array[String]] = []) -> GridTrackList {
  let tracks = lines.map(named_lines)
  let values = [literal_expression("subgrid", "grid-track")]
  for track in tracks {
    values.push(track.expression)
  }
  let expression = group_expression("grid-track-list", "", values, " ", "")
  { css: expression.to_css(), expression, tracks, subgrid: true, }
}

///|
pub fn GridTrackList::expression(self : GridTrackList) -> Expression {
  if self.auto_tracks_allowed() {
    self.expression
  } else {
    { ..self.expression, category: "grid-template-track-list", }
  }
}

///|
pub fn GridTrackList::auto_tracks_allowed(self : GridTrackList) -> Bool {
  !self.subgrid &&
  self.tracks.every(track => {
    match track.kind {
      Named | Repeat(_, _) => false
      _ => true
    }
  })
}

///|
pub fn invalid_expression(code : String, message : String) -> Expression {
  literal_expression(issue_css(code, message), "invalid")
}