///|
fn clean(input : StringView) -> String {
  input.trim().to_owned()
}

///|
fn split_char(input : String, sep : Char) -> Array[String] {
  let parts : Array[String] = []
  let buf = StringBuilder(size_hint=input.length())
  for c in input.iter() {
    if c == sep {
      parts.push(buf.to_string())
      buf.reset()
    } else {
      buf.write_char(c)
    }
  }
  parts.push(buf.to_string())
  parts
}

///|
fn split_quoted(input : String, sep : Char) -> Array[String] {
  let parts : Array[String] = []
  let buf = StringBuilder(size_hint=input.length())
  let mut quoted = false
  let mut escaped = false
  for c in input.iter() {
    if escaped {
      buf.write_char(c)
      escaped = false
    } else if quoted && c == '\\' {
      buf.write_char(c)
      escaped = true
    } else if c == '"' {
      quoted = !quoted
      buf.write_char(c)
    } else if !quoted && c == sep {
      parts.push(buf.to_string())
      buf.reset()
    } else {
      buf.write_char(c)
    }
  }
  parts.push(buf.to_string())
  parts
}

///|
fn digit_value(c : Char) -> Int? {
  let code = c.to_int()
  if code >= 48 && code <= 57 {
    Some(code - 48)
  } else {
    None
  }
}

///|
fn normalize_newlines(input : String) -> String {
  input.replace_all(old="\r\n", new="\n").replace_all(old="\r", new="\n")
}

///|
fn parse_int_text(raw : String) -> Int? {
  let text = clean(raw[:])
  if text == "" {
    return None
  }
  let mut sign = 1
  let mut index = 0
  match text.get_char(0) {
    Some('-') => {
      sign = -1
      index = 1
    }
    Some('+') => index = 1
    _ => ()
  }
  if index >= text.length() {
    return None
  }
  let mut value = 0
  let mut digits = 0
  for i in index..
        match digit_value(c) {
          Some(d) => {
            value = value * 10 + d
            digits += 1
          }
          None => return None
        }
      None => return None
    }
  }
  if digits == 0 {
    None
  } else {
    Some(sign * value)
  }
}

///|
fn parse_unsigned_int(
  raw : String,
  line : Int,
  field : String,
) -> Result[Int, ParseError] {
  match parse_int_text(raw) {
    Some(value) =>
      if value >= 0 {
        Ok(value)
      } else {
        Err(InvalidNumber(line, field, raw))
      }
    None => Err(InvalidNumber(line, field, raw))
  }
}

///|
fn parse_millis(raw : String, line : Int, field : String) -> Result[Int, ParseError] {
  match parse_millis_text(raw) {
    Some(value) =>
      if value >= 0 {
        Ok(value)
      } else {
        Err(InvalidNumber(line, field, raw))
      }
    None => Err(InvalidNumber(line, field, raw))
  }
}

///|
fn parse_signed_millis(
  raw : String,
  line : Int,
  field : String,
) -> Result[Int, ParseError] {
  match parse_millis_text(raw) {
    Some(value) => Ok(value)
    None => Err(InvalidNumber(line, field, raw))
  }
}

///|
fn parse_millis_text(raw : String) -> Int? {
  let text = clean(raw[:])
  if text == "" {
    return None
  }
  let mut sign = 1
  let mut index = 0
  match text.get_char(0) {
    Some('-') => {
      sign = -1
      index = 1
    }
    Some('+') => index = 1
    _ => ()
  }
  let mut whole = 0
  let mut whole_digits = 0
  while index < text.length() {
    match text.get_char(index) {
      Some('.') => break
      Some(c) =>
        match digit_value(c) {
          Some(d) => {
            whole = whole * 10 + d
            whole_digits += 1
            index += 1
          }
          None => return None
        }
      None => return None
    }
  }
  if whole_digits == 0 {
    return None
  }
  let mut frac = 0
  let mut frac_digits = 0
  if index < text.length() {
    if text.get_char(index) != Some('.') {
      return None
    }
    index += 1
    while index < text.length() {
      match text.get_char(index) {
        Some(c) =>
          match digit_value(c) {
            Some(d) => {
              if frac_digits < 3 {
                frac = frac * 10 + d
                frac_digits += 1
              }
              index += 1
            }
            None => return None
          }
        None => return None
      }
    }
  }
  while frac_digits < 3 {
    frac = frac * 10
    frac_digits += 1
  }
  Some(sign * (whole * 1000 + frac))
}

///|
fn pad3(value : Int) -> String {
  let v = if value < 0 { -value } else { value }
  let buf = StringBuilder(size_hint=3)
  if v < 100 {
    buf.write_char('0')
  }
  if v < 10 {
    buf.write_char('0')
  }
  buf.write_string(v.to_string())
  buf.to_string()
}

///|
pub fn millis_to_string(ms : Int) -> String {
  let sign = if ms < 0 { "-" } else { "" }
  let value = if ms < 0 { -ms } else { ms }
  let whole = value / 1000
  let frac = value % 1000
  if frac == 0 {
    sign + whole.to_string() + ".000"
  } else {
    let mut text = pad3(frac)
    while text.length() > 1 && text.get_char(text.length() - 1) == Some('0') {
      text = text[0:text.length() - 1].to_owned()
    }
    sign + whole.to_string() + "." + text
  }
}

///|
fn yes_no(value : Bool) -> String {
  if value {
    "YES"
  } else {
    "NO"
  }
}

///|
fn parse_yes_no(value : String) -> Bool {
  value.to_upper() == "YES"
}

///|
fn maybe_attr(attrs : Array[HlsAttribute], name : String) -> String? {
  let wanted = name.to_upper()
  for attr in attrs {
    if attr.name == wanted {
      return Some(attr.value)
    }
  }
  None
}

///|
fn maybe_int_attr(
  attrs : Array[HlsAttribute],
  name : String,
  line : Int,
) -> Result[Int?, ParseError] {
  match maybe_attr(attrs, name) {
    Some(value) =>
      match parse_unsigned_int(value, line, name) {
        Ok(parsed) => Ok(Some(parsed))
        Err(e) => Err(e)
      }
    None => Ok(None)
  }
}

///|
fn required_attr(
  attrs : Array[HlsAttribute],
  name : String,
  line : Int,
) -> Result[String, ParseError] {
  match maybe_attr(attrs, name) {
    Some(value) =>
      if value == "" {
        Err(InvalidAttribute(line, "attribute \{name} must not be empty"))
      } else {
        Ok(value)
      }
    None => Err(InvalidAttribute(line, "missing required attribute \{name}"))
  }
}

///|
fn push_unknown(unknown : Array[String], line : Int, text : String) -> Unit {
  unknown.push("\{line}: \{text}")
}