///|
fn dict_clear(
  dict : Ref[Map[String, @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~,
    )
  }
  dict.val = {}
  @value.Value::None
}

///|
fn dict_get(
  dict : Ref[Map[String, @value.Value]],
  pos : Array[@value.Value],
  _kw : Map[String, @value.Value],
  span : @ast.Span,
) -> @value.Value raise StarlarkError {
  guard pos.length() >= 1 && pos.length() <= 2 else {
    raise StarlarkError::TypeError(
      message="get() takes at least 1 and at most 2 arguments (\{pos.length()} given)",
      span~,
    )
  }
  let key = match pos[0] {
    String(s) => s
    _ =>
      raise StarlarkError::TypeError(
        message="dict key must be a string, got '\{pos[0].type_name()}'",
        span~,
      )
  }
  let default = if pos.length() >= 2 { pos[1] } else { @value.Value::None }
  match dict.val.get(key) {
    Some(v) => v
    None => default
  }
}

///|
fn dict_items(
  dict : Ref[Map[String, @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="items() takes no arguments (\{pos.length()} given)",
      span~,
    )
  }
  let result : Array[@value.Value] = []
  for k, v in dict.val {
    result.push(@value.Value::Tuple([@value.Value::String(k), v]))
  }
  @value.Value::List(Ref::new(result))
}

///|
fn dict_keys(
  dict : Ref[Map[String, @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="keys() takes no arguments (\{pos.length()} given)",
      span~,
    )
  }
  let result : Array[@value.Value] = []
  for k, _ in dict.val {
    result.push(@value.Value::String(k))
  }
  @value.Value::List(Ref::new(result))
}

///|
fn dict_values(
  dict : Ref[Map[String, @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="values() takes no arguments (\{pos.length()} given)",
      span~,
    )
  }
  let result : Array[@value.Value] = []
  for _, v in dict.val {
    result.push(v)
  }
  @value.Value::List(Ref::new(result))
}

///|
fn dict_pop(
  dict : Ref[Map[String, @value.Value]],
  pos : Array[@value.Value],
  _kw : Map[String, @value.Value],
  span : @ast.Span,
) -> @value.Value raise StarlarkError {
  guard pos.length() >= 1 && pos.length() <= 2 else {
    raise StarlarkError::TypeError(
      message="pop() takes at least 1 and at most 2 arguments (\{pos.length()} given)",
      span~,
    )
  }
  let key = match pos[0] {
    String(s) => s
    _ =>
      raise StarlarkError::TypeError(
        message="dict key must be a string, got '\{pos[0].type_name()}'",
        span~,
      )
  }
  match dict.val.get(key) {
    Some(v) => {
      dict.val.remove(key)
      v
    }
    None =>
      if pos.length() >= 2 {
        pos[1]
      } else {
        raise StarlarkError::KeyError(key~, span~)
      }
  }
}

///|
fn dict_popitem(
  dict : Ref[Map[String, @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="popitem() takes no arguments (\{pos.length()} given)",
      span~,
    )
  }
  if dict.val.length() == 0 {
    raise StarlarkError::KeyError(key="popitem(): dictionary is empty", span~)
  }
  // Get the first key-value pair
  let mut first_key = ""
  let mut first_val : @value.Value = @value.Value::None
  for k, v in dict.val {
    first_key = k
    first_val = v
    break
  }
  dict.val.remove(first_key)
  @value.Value::Tuple([@value.Value::String(first_key), first_val])
}

///|
fn dict_setdefault(
  dict : Ref[Map[String, @value.Value]],
  pos : Array[@value.Value],
  _kw : Map[String, @value.Value],
  span : @ast.Span,
) -> @value.Value raise StarlarkError {
  guard pos.length() >= 1 && pos.length() <= 2 else {
    raise StarlarkError::TypeError(
      message="setdefault() takes at least 1 and at most 2 arguments (\{pos.length()} given)",
      span~,
    )
  }
  let key = match pos[0] {
    String(s) => s
    _ =>
      raise StarlarkError::TypeError(
        message="dict key must be a string, got '\{pos[0].type_name()}'",
        span~,
      )
  }
  let default = if pos.length() >= 2 { pos[1] } else { @value.Value::None }
  match dict.val.get(key) {
    Some(v) => v
    None => {
      dict.val.set(key, default)
      default
    }
  }
}

///|
fn dict_update(
  dict : Ref[Map[String, @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="update() takes at most 1 positional argument (\{pos.length()} given)",
      span~,
    )
  }
  if pos.length() == 1 {
    match pos[0] {
      Dict(other) =>
        for k, v in other.val {
          dict.val.set(k, v)
        }
      _ => {
        // Try iterable of pairs
        let items = iterable_to_array(pos[0], span)
        for item in items {
          match item {
            Tuple(pair) =>
              if pair.length() == 2 {
                match pair[0] {
                  String(k) => dict.val.set(k, pair[1])
                  _ =>
                    raise StarlarkError::TypeError(
                      message="dict keys must be strings",
                      span~,
                    )
                }
              } else {
                raise StarlarkError::ValueError(
                  message="dictionary update sequence element has length \{pair.length()}; 2 is required",
                  span~,
                )
              }
            List(pair) =>
              if pair.val.length() == 2 {
                match pair.val[0] {
                  String(k) => dict.val.set(k, pair.val[1])
                  _ =>
                    raise StarlarkError::TypeError(
                      message="dict keys must be strings",
                      span~,
                    )
                }
              } else {
                raise StarlarkError::ValueError(
                  message="dictionary update sequence element has length \{pair.val.length()}; 2 is required",
                  span~,
                )
              }
            _ =>
              raise StarlarkError::TypeError(
                message="cannot convert dictionary update element to a pair",
                span~,
              )
          }
        }
      }
    }
  }
  // Also apply keyword args
  for k, v in kw {
    dict.val.set(k, v)
  }
  @value.Value::None
}

///|
fn dict_methods() -> Map[
  String,
  (
    Ref[Map[String, @value.Value]],
    Array[@value.Value],
    Map[String, @value.Value],
    @ast.Span,
  ) -> @value.Value raise StarlarkError,
] {
  let m : Map[
    String,
    (
      Ref[Map[String, @value.Value]],
      Array[@value.Value],
      Map[String, @value.Value],
      @ast.Span,
    ) -> @value.Value raise StarlarkError,
  ] = {}
  m["clear"] = dict_clear
  m["get"] = dict_get
  m["items"] = dict_items
  m["keys"] = dict_keys
  m["values"] = dict_values
  m["pop"] = dict_pop
  m["popitem"] = dict_popitem
  m["setdefault"] = dict_setdefault
  m["update"] = dict_update
  m
}