///|
fn list_append(
  list : Ref[Array[@value.Value]],
  pos : Array[@value.Value],
  _kw : Map[String, @value.Value],
  span : @ast.Span,
) -> @value.Value raise StarlarkError {
  guard pos.length() == 1 else {
    raise StarlarkError::TypeError(
      message="append() takes exactly one argument (\{pos.length()} given)",
      span~,
    )
  }
  list.val.push(pos[0])
  @value.Value::None
}

///|
fn list_clear(
  list : Ref[Array[@value.Value]],
  pos : Array[@value.Value],
  _kw : Map[String, @value.Value],
  span : @ast.Span,
) -> @value.Value raise StarlarkError {
  guard pos.length() == 0 else {
    raise StarlarkError::TypeError(
      message="clear() takes no arguments (\{pos.length()} given)",
      span~,
    )
  }
  list.val.clear()
  @value.Value::None
}

///|
fn list_extend(
  list : Ref[Array[@value.Value]],
  pos : Array[@value.Value],
  _kw : Map[String, @value.Value],
  span : @ast.Span,
) -> @value.Value raise StarlarkError {
  guard pos.length() == 1 else {
    raise StarlarkError::TypeError(
      message="extend() takes exactly one argument (\{pos.length()} given)",
      span~,
    )
  }
  let items = iterable_to_array(pos[0], span)
  for item in items {
    list.val.push(item)
  }
  @value.Value::None
}

///|
fn list_index(
  list : Ref[Array[@value.Value]],
  pos : Array[@value.Value],
  _kw : Map[String, @value.Value],
  span : @ast.Span,
) -> @value.Value raise StarlarkError {
  guard pos.length() >= 1 && pos.length() <= 3 else {
    raise StarlarkError::TypeError(
      message="index() takes at least 1 and at most 3 arguments (\{pos.length()} given)",
      span~,
    )
  }
  let x = pos[0]
  let start = if pos.length() >= 2 {
    clamp_list_index(expect_int(pos[1], "index", span), list.val.length())
  } else {
    0
  }
  let end = if pos.length() >= 3 {
    clamp_list_index(expect_int(pos[2], "index", span), list.val.length())
  } else {
    list.val.length()
  }
  for i = start; i < end; i = i + 1 {
    if values_equal(list.val[i], x) {
      return @value.Value::Int(i.to_int64())
    }
  }
  raise StarlarkError::ValueError(
    message="\{value_to_repr(x)} is not in list",
    span~,
  )
}

///|
fn list_insert(
  list : Ref[Array[@value.Value]],
  pos : Array[@value.Value],
  _kw : Map[String, @value.Value],
  span : @ast.Span,
) -> @value.Value raise StarlarkError {
  guard pos.length() == 2 else {
    raise StarlarkError::TypeError(
      message="insert() takes exactly 2 arguments (\{pos.length()} given)",
      span~,
    )
  }
  let idx_raw = expect_int(pos[0], "insert", span)
  let x = pos[1]
  let len = list.val.length()
  let idx = if idx_raw < 0 {
    let i = idx_raw + len
    if i < 0 {
      0
    } else {
      i
    }
  } else if idx_raw > len {
    len
  } else {
    idx_raw
  }
  // Insert by shifting elements
  list.val.push(@value.Value::None) // extend by 1
  for i = list.val.length() - 1; i > idx; i = i - 1 {
    list.val[i] = list.val[i - 1]
  }
  list.val[idx] = x
  @value.Value::None
}

///|
fn list_pop(
  list : Ref[Array[@value.Value]],
  pos : Array[@value.Value],
  _kw : Map[String, @value.Value],
  span : @ast.Span,
) -> @value.Value raise StarlarkError {
  guard pos.length() <= 1 else {
    raise StarlarkError::TypeError(
      message="pop() takes at most 1 argument (\{pos.length()} given)",
      span~,
    )
  }
  if list.val.length() == 0 {
    raise StarlarkError::IndexError(message="pop from empty list", span~)
  }
  let idx_raw = if pos.length() == 1 {
    expect_int(pos[0], "pop", span)
  } else {
    -1
  }
  let len = list.val.length()
  let idx = if idx_raw < 0 { idx_raw + len } else { idx_raw }
  if idx < 0 || idx >= len {
    raise StarlarkError::IndexError(message="pop index out of range", span~)
  }
  let val = list.val[idx]
  // Shift elements left
  for i = idx; i < len - 1; i = i + 1 {
    list.val[i] = list.val[i + 1]
  }
  let _ = list.val.pop()
  val
}

///|
fn list_remove(
  list : Ref[Array[@value.Value]],
  pos : Array[@value.Value],
  _kw : Map[String, @value.Value],
  span : @ast.Span,
) -> @value.Value raise StarlarkError {
  guard pos.length() == 1 else {
    raise StarlarkError::TypeError(
      message="remove() takes exactly 1 argument (\{pos.length()} given)",
      span~,
    )
  }
  let x = pos[0]
  let mut found = -1
  for i = 0; i < list.val.length(); i = i + 1 {
    if values_equal(list.val[i], x) {
      found = i
      break
    }
  }
  if found < 0 {
    raise StarlarkError::ValueError(
      message="list.remove(x): x not in list",
      span~,
    )
  }
  // Shift elements left
  for i = found; i < list.val.length() - 1; i = i + 1 {
    list.val[i] = list.val[i + 1]
  }
  let _ = list.val.pop()
  @value.Value::None
}

///|
fn list_methods() -> Map[
  String,
  (
    Ref[Array[@value.Value]],
    Array[@value.Value],
    Map[String, @value.Value],
    @ast.Span,
  ) -> @value.Value raise StarlarkError,
] {
  let m : Map[
    String,
    (
      Ref[Array[@value.Value]],
      Array[@value.Value],
      Map[String, @value.Value],
      @ast.Span,
    ) -> @value.Value raise StarlarkError,
  ] = {}
  m["append"] = list_append
  m["clear"] = list_clear
  m["extend"] = list_extend
  m["index"] = list_index
  m["insert"] = list_insert
  m["pop"] = list_pop
  m["remove"] = list_remove
  m
}

///|
fn clamp_list_index(idx : Int, len : Int) -> Int {
  if idx < 0 {
    let result = idx + len
    if result < 0 {
      0
    } else {
      result
    }
  } else if idx > len {
    len
  } else {
    idx
  }
}