///|
pub fn parse_attribute_list(
  raw : String,
  line~ : Int = 0,
) -> Result[Array[HlsAttribute], ParseError] {
  let attrs : Array[HlsAttribute] = []
  for piece in split_quoted(raw, ',') {
    let item = clean(piece[:])
    if item == "" {
      continue
    }
    match item.split_once("=") {
      Some((name_view, value_view)) => {
        let name = clean(name_view).to_upper()
        let raw_value = clean(value_view)
        if name == "" {
          return Err(InvalidAttribute(line, "attribute name is empty"))
        }
        attrs.push({
          name,
          value: unquote(raw_value),
          quoted: is_quoted(raw_value),
        })
      }
      None =>
        return Err(InvalidAttribute(line, "expected KEY=VALUE, got \{item}"))
    }
  }
  Ok(attrs)
}

///|
fn is_quoted(input : String) -> Bool {
  input.length() >= 2 &&
  input.get_char(0) == Some('"') &&
  input.get_char(input.length() - 1) == Some('"')
}

///|
fn unquote(input : String) -> String {
  if !is_quoted(input) {
    return input
  }
  let inner = input[1:input.length() - 1].to_owned()
  let out = StringBuilder(size_hint=inner.length())
  let mut escaped = false
  for c in inner.iter() {
    if escaped {
      out.write_char(c)
      escaped = false
    } else if c == '\\' {
      escaped = true
    } else {
      out.write_char(c)
    }
  }
  if escaped {
    out.write_char('\\')
  }
  out.to_string()
}

///|
fn quote_attr(value : String) -> String {
  let out = StringBuilder(size_hint=value.length() + 2)
  out.write_char('"')
  for c in value.iter() {
    if c == '"' || c == '\\' {
      out.write_char('\\')
    }
    out.write_char(c)
  }
  out.write_char('"')
  out.to_string()
}

///|
fn write_attr(
  attrs : Array[String],
  name : String,
  value : String,
  quoted~ : Bool = false,
) -> Unit {
  if quoted {
    attrs.push(name + "=" + quote_attr(value))
  } else {
    attrs.push(name + "=" + value)
  }
}

///|
fn join_attrs(attrs : Array[String]) -> String {
  let buf = StringBuilder()
  for i, attr in attrs {
    if i > 0 {
      buf.write_char(',')
    }
    buf.write_string(attr)
  }
  buf.to_string()
}

///|
pub fn attributes_to_string(attrs : Array[HlsAttribute]) -> String {
  let parts : Array[String] = []
  for attr in attrs {
    write_attr(parts, attr.name, attr.value, quoted=attr.quoted)
  }
  join_attrs(parts)
}

///|
fn parse_byte_range(raw : String, line : Int) -> Result[ByteRange, ParseError] {
  match clean(raw[:]).split_once("@") {
    Some((length_view, offset_view)) =>
      match parse_unsigned_int(clean(length_view), line, "BYTERANGE.length") {
        Ok(length) =>
          match parse_unsigned_int(clean(offset_view), line, "BYTERANGE.offset") {
            Ok(offset) => Ok({ length, offset: Some(offset) })
            Err(err) => Err(err)
          }
        Err(err) => Err(err)
      }
    None =>
      match parse_unsigned_int(clean(raw[:]), line, "BYTERANGE.length") {
        Ok(length) => Ok({ length, offset: None })
        Err(err) => Err(err)
      }
  }
}

///|
fn byte_range_to_string(range : ByteRange) -> String {
  match range.offset {
    Some(offset) => range.length.to_string() + "@" + offset.to_string()
    None => range.length.to_string()
  }
}

///|
fn parse_resolution(raw : String, line : Int) -> Result[Resolution, ParseError] {
  match clean(raw[:]).split_once("x") {
    Some((w, h)) =>
      match parse_unsigned_int(clean(w), line, "RESOLUTION.width") {
        Ok(width) =>
          match parse_unsigned_int(clean(h), line, "RESOLUTION.height") {
            Ok(height) => Ok({ width, height })
            Err(err) => Err(err)
          }
        Err(err) => Err(err)
      }
    None => Err(InvalidNumber(line, "RESOLUTION", raw))
  }
}

///|
fn resolution_to_string(resolution : Resolution) -> String {
  resolution.width.to_string() + "x" + resolution.height.to_string()
}