///|
fn parse_header_with_options(
  header : String,
  strict? : Bool = false,
) -> Result[Disposition, ParseError] {
  let diagnostics : Array[Diagnostic] = []
  let trimmed = header.trim(chars=" \t\r\n").to_owned()
  if trimmed.is_empty() {
    let d = Diagnostic::make(
      EmptyHeader,
      Error,
      "Content-Disposition header is empty",
      Some(0),
    )
    return Err(ParseError::from_diagnostic(d))
  }
  let (kind_token_view, rest_view) = match trimmed.split_once(";") {
    Some((a, b)) => (a.trim(chars=" \t"), Some(b))
    None => (trimmed[:].trim(chars=" \t"), None)
  }
  let kind_token = kind_token_view.to_owned()
  if !is_token(kind_token) {
    let d = Diagnostic::make(
      InvalidDisposition,
      Error,
      "Disposition type must be a non-empty HTTP token",
      Some(0),
    )
    return Err(ParseError::from_diagnostic(d))
  }
  let params = match rest_view {
    Some(rest) =>
      parse_parameters(
        rest.to_owned(),
        diagnostics,
        base_offset=kind_token.length() + 1,
      )
    None => []
  }
  let disposition : Disposition = {
    kind: DispositionKind::from_token(kind_token),
    params,
    diagnostics,
  }
  if strict && disposition.has_errors() {
    Err(ParseError::from_diagnostic(first_error(disposition.diagnostics)))
  } else {
    Ok(disposition)
  }
}

///|
fn first_error(diagnostics : Array[Diagnostic]) -> Diagnostic {
  for d in diagnostics {
    if d.severity == Error {
      return d
    }
  }
  Diagnostic::make(InvalidDisposition, Error, "Unknown parse error", None)
}

///|
fn push_diag(
  diagnostics : Array[Diagnostic],
  code : DiagnosticCode,
  severity : Severity,
  message : String,
  offset : Int?,
) -> Unit {
  diagnostics.push(Diagnostic::make(code, severity, message, offset))
}

///|
fn parse_parameters(
  input : String,
  diagnostics : Array[Diagnostic],
  base_offset? : Int = 0,
) -> Array[Parameter] {
  let params : Array[Parameter] = []
  let mut index = 0
  let len = input.length()
  while index < len {
    index = skip_delimiters(input, index)
    if index >= len {
      break
    }
    let name_start = index
    while index < len &&
          char_at(input, index) != '=' &&
          char_at(input, index) != ';' {
      index = index + 1
    }
    let raw_name = input[name_start:index].trim(chars=" \t").to_owned()
    if index >= len || char_at(input, index) != '=' {
      push_diag(
        diagnostics,
        MissingEquals,
        Error,
        "Parameter is missing '=' after its name",
        Some(base_offset + name_start),
      )
      index = skip_to_next_parameter(input, index)
      continue
    }
    index = index + 1
    let value_start = index
    let parsed = parse_value(input, index, diagnostics, base_offset)
    let raw_value = input[value_start:parsed.next].trim(chars=" \t").to_owned()
    index = parsed.next
    let extended = raw_name.has_suffix("*")
    let normalized = normalize_param_name(raw_name)
    if !is_token(normalized) {
      push_diag(
        diagnostics,
        InvalidParameterName,
        Error,
        "Parameter name must be a HTTP token",
        Some(base_offset + name_start),
      )
      index = skip_to_next_parameter(input, index)
      continue
    }
    if has_duplicate(params, normalized, extended) {
      push_diag(
        diagnostics,
        DuplicateParameter,
        Warning,
        "Duplicate parameter with the same normalized name",
        Some(base_offset + name_start),
      )
    }
    let decoded = if extended {
      decode_extended_value(
        parsed.value,
        diagnostics,
        base_offset + value_start,
      )
    } else {
      { value: parsed.value, charset: None, language: None }
    }
    params.push({
      name: normalized,
      value: decoded.value,
      raw_name,
      raw_value,
      extended,
      charset: decoded.charset,
      language: decoded.language,
      position: params.length(),
    })
    index = skip_to_next_parameter(input, index)
  }
  params
}

///|
struct ParsedValue {
  value : String
  next : Int
} derive(Eq, Debug)

///|
fn parse_value(
  input : String,
  start : Int,
  diagnostics : Array[Diagnostic],
  base_offset : Int,
) -> ParsedValue {
  let mut index = skip_ows(input, start)
  if index >= input.length() {
    push_diag(
      diagnostics,
      MissingValue,
      Error,
      "Parameter value is missing",
      Some(base_offset + start),
    )
    return { value: "", next: index }
  }
  if char_at(input, index) == '"' {
    parse_quoted_value(input, index, diagnostics, base_offset)
  } else {
    let value_start = index
    while index < input.length() && char_at(input, index) != ';' {
      index = index + 1
    }
    {
      value: input[value_start:index].trim(chars=" \t").to_owned(),
      next: index,
    }
  }
}

///|
fn parse_quoted_value(
  input : String,
  quote_start : Int,
  diagnostics : Array[Diagnostic],
  base_offset : Int,
) -> ParsedValue {
  let buf = StringBuilder(size_hint=32)
  let mut index = quote_start + 1
  let mut closed = false
  while index < input.length() {
    let c = char_at(input, index)
    if c == '"' {
      closed = true
      index = index + 1
      break
    } else if c == '\\' {
      if index + 1 >= input.length() {
        push_diag(
          diagnostics,
          BadEscape,
          Error,
          "Quoted string ends after a backslash escape",
          Some(base_offset + index),
        )
        index = index + 1
        break
      }
      let escaped = char_at(input, index + 1)
      if escaped == '\r' || escaped == '\n' {
        push_diag(
          diagnostics,
          BadEscape,
          Error,
          "Quoted string escape cannot contain a line break",
          Some(base_offset + index),
        )
      } else {
        buf.write_char(escaped)
      }
      index = index + 2
    } else {
      if c == '\r' || c == '\n' {
        push_diag(
          diagnostics,
          ControlCharacter,
          Error,
          "Quoted string cannot contain a raw line break",
          Some(base_offset + index),
        )
      } else {
        buf.write_char(c)
      }
      index = index + 1
    }
  }
  if !closed {
    push_diag(
      diagnostics,
      UnterminatedQuote,
      Error,
      "Quoted string is not terminated",
      Some(base_offset + quote_start),
    )
  }
  while index < input.length() && char_at(input, index) != ';' {
    if !is_ows(char_at(input, index)) {
      push_diag(
        diagnostics,
        InvalidParameterName,
        Warning,
        "Ignoring non-whitespace characters after quoted value",
        Some(base_offset + index),
      )
    }
    index = index + 1
  }
  { value: buf.to_string(), next: index }
}

///|
fn skip_ows(input : String, index : Int) -> Int {
  let mut i = index
  while i < input.length() && is_ows(char_at(input, i)) {
    i = i + 1
  }
  i
}

///|
fn skip_delimiters(input : String, index : Int) -> Int {
  let mut i = index
  while i < input.length() {
    let c = char_at(input, i)
    if c == ';' || is_ows(c) {
      i = i + 1
    } else {
      break
    }
  }
  i
}

///|
fn skip_to_next_parameter(input : String, index : Int) -> Int {
  let mut i = index
  while i < input.length() && char_at(input, i) != ';' {
    i = i + 1
  }
  if i < input.length() && char_at(input, i) == ';' {
    i + 1
  } else {
    i
  }
}

///|
fn has_duplicate(
  params : Array[Parameter],
  name : String,
  extended : Bool,
) -> Bool {
  params.any(p => p.name == name && p.extended == extended)
}

///|
struct DecodedValue {
  value : String
  charset : String?
  language : String?
} derive(Eq, Debug)

///|
fn decode_extended_value(
  raw : String,
  diagnostics : Array[Diagnostic],
  offset : Int,
) -> DecodedValue {
  match raw.split_once("'") {
    Some((charset_view, rest_view)) =>
      match rest_view.split_once("'") {
        Some((language_view, data_view)) => {
          let charset = charset_view.to_owned()
          let language = language_view.to_owned()
          let data = data_view.to_owned()
          let decoded = decode_percent_data(data, charset, diagnostics, offset)
          { value: decoded, charset: Some(charset), language: Some(language) }
        }
        None => {
          push_diag(
            diagnostics,
            InvalidExtendedValue,
            Error,
            "Extended value must have charset'language'value sections",
            Some(offset),
          )
          { value: raw, charset: Some(charset_view.to_owned()), language: None }
        }
      }
    None => {
      push_diag(
        diagnostics,
        InvalidExtendedValue,
        Error,
        "Extended value must start with a charset section",
        Some(offset),
      )
      { value: raw, charset: None, language: None }
    }
  }
}

///|
fn decode_percent_data(
  data : String,
  charset : String,
  diagnostics : Array[Diagnostic],
  offset : Int,
) -> String {
  let bytes = percent_decode_bytes(data, diagnostics, offset)
  let lower = charset.to_lower()
  if lower == "utf-8" || lower == "utf8" {
    @utf8.decode_lossy(bytes)
  } else if lower == "iso-8859-1" || lower == "latin1" || lower == "latin-1" {
    let chars : Array[Char] = []
    for byte in bytes.iter() {
      chars.push(byte.to_char())
    }
    String::from_array(chars)
  } else {
    push_diag(
      diagnostics,
      UnsupportedCharset,
      Error,
      "Unsupported extended filename charset: " + charset,
      Some(offset),
    )
    data
  }
}

///|
fn percent_decode_bytes(
  data : String,
  diagnostics : Array[Diagnostic],
  offset : Int,
) -> Bytes {
  let values : Array[Byte] = []
  let mut index = 0
  while index < data.length() {
    let c = char_at(data, index)
    if c == '%' {
      if index + 2 >= data.length() ||
        !is_hex(char_at(data, index + 1)) ||
        !is_hex(char_at(data, index + 2)) {
        push_diag(
          diagnostics,
          InvalidPercentEncoding,
          Error,
          "Percent escape must contain two hexadecimal digits",
          Some(offset + index),
        )
        values.push('%'.to_int().to_byte())
        index = index + 1
      } else {
        let hi = hex_value(char_at(data, index + 1))
        let lo = hex_value(char_at(data, index + 2))
        values.push(((hi << 4) + lo).to_byte())
        index = index + 3
      }
    } else {
      let bs = @utf8.encode(c.to_string())
      for b in bs.iter() {
        values.push(b)
      }
      index = index + c.utf16_len()
    }
  }
  Bytes::from_array(values)
}