///|
fn is_etag_char(c : Char) -> Bool {
  let code = c.to_int()
  code == 33 || (code >= 35 && code <= 126) || code >= 128
}

///|
fn parse_opaque_tag(text : String, item : Int) -> Result[String, ParseError] {
  if text.length() < 2 ||
    text.get_char(0) != Some('"') ||
    text.get_char(text.length() - 1) != Some('"') {
    return Err(InvalidParameter(item, text))
  }
  let inner = text[1:text.length() - 1].to_owned()
  for c in inner.iter() {
    if !is_etag_char(c) {
      return Err(InvalidToken(item, "etag", text))
    }
  }
  Ok(inner)
}

///|
pub fn parse_etag(value : StringView) -> Result[EntityTag, ParseError] {
  let raw = clean(value)
  if raw.has_prefix("W/") || raw.has_prefix("w/") {
    let rest = raw[2:].to_owned()
    match parse_opaque_tag(rest, 0) {
      Ok(tag) => Ok({ weak: true, tag, raw })
      Err(err) => Err(err)
    }
  } else {
    match parse_opaque_tag(raw, 0) {
      Ok(tag) => Ok({ weak: false, tag, raw })
      Err(err) => Err(err)
    }
  }
}

///|
pub fn parse_etag_list(
  value : StringView,
) -> Result[Array[EntityTag], ParseError] {
  let text = clean(value)
  let tags : Array[EntityTag] = []
  if text.is_empty() {
    return Ok(tags)
  }
  if text == "*" {
    return Ok([{ weak: false, tag: "*", raw: "*" }])
  }
  let parts = split_quoted(text, ',')
  for i in 0.. tags.push(tag)
      Err(_) => return Err(InvalidParameter(i, part))
    }
  }
  Ok(tags)
}

///|
pub fn format_etag(tag : EntityTag) -> String {
  if tag.raw == "*" {
    return "*"
  }
  let buf = StringBuilder()
  if tag.weak {
    buf.write_string("W/")
  }
  buf.write_char('"')
  buf.write_string(tag.tag)
  buf.write_char('"')
  buf.to_string()
}

///|
pub fn strong_etag_match(left : EntityTag, right : EntityTag) -> Bool {
  !left.weak && !right.weak && left.tag == right.tag
}

///|
pub fn weak_etag_match(left : EntityTag, right : EntityTag) -> Bool {
  left.tag == right.tag
}

///|
pub fn etag_list_matches(
  header_value : StringView,
  current : EntityTag,
  strong? : Bool = false,
) -> Result[Bool, ParseError] {
  let tags = match parse_etag_list(header_value) {
    Ok(tags) => tags
    Err(err) => return Err(err)
  }
  for tag in tags {
    if tag.raw == "*" {
      return Ok(true)
    }
    if strong {
      if strong_etag_match(tag, current) {
        return Ok(true)
      }
    } else if weak_etag_match(tag, current) {
      return Ok(true)
    }
  }
  Ok(false)
}

///|
pub fn evaluate_if_none_match(
  http_method : StringView,
  header_value : StringView,
  current : EntityTag,
) -> Result[ConditionalDecision, ParseError] {
  let matched = match etag_list_matches(header_value, current) {
    Ok(value) => value
    Err(err) => return Err(err)
  }
  if !matched {
    return Ok({
      matched: false,
      status: 200,
      reason: "if-none-match did not match current entity tag",
    })
  }
  let normalized_method = clean(http_method).to_upper()
  if normalized_method == "GET" || normalized_method == "HEAD" {
    Ok({
      matched: true,
      status: 304,
      reason: "if-none-match matched for safe retrieval method",
    })
  } else {
    Ok({
      matched: true,
      status: 412,
      reason: "if-none-match matched for state-changing method",
    })
  }
}

///|
pub fn evaluate_if_match(
  header_value : StringView,
  current : EntityTag,
) -> Result[ConditionalDecision, ParseError] {
  let matched = match etag_list_matches(header_value, current, strong=true) {
    Ok(value) => value
    Err(err) => return Err(err)
  }
  if matched {
    Ok({
      matched: true,
      status: 200,
      reason: "if-match matched current entity tag",
    })
  } else {
    Ok({
      matched: false,
      status: 412,
      reason: "if-match did not match current entity tag",
    })
  }
}

///|
pub fn format_conditional_decision(decision : ConditionalDecision) -> String {
  let buf = StringBuilder()
  buf.write_string("matched=")
  buf.write_string(decision.matched.to_string())
  buf.write_string(", status=")
  buf.write_string(decision.status.to_string())
  buf.write_string(", ")
  buf.write_string(decision.reason)
  buf.to_string()
}