///|
pub fn RequestPreferences::RequestPreferences(
  accept? : StringView = "",
  accept_language? : StringView = "",
  accept_encoding? : StringView = "",
  accept_charset? : StringView = "",
) -> RequestPreferences {
  {
    accept: accept.to_owned(),
    accept_language: accept_language.to_owned(),
    accept_encoding: accept_encoding.to_owned(),
    accept_charset: accept_charset.to_owned(),
  }
}

///|
pub fn ServerVariant::ServerVariant(
  id : StringView,
  media : StringView,
  language? : StringView,
  encoding? : StringView,
  charset? : StringView,
  server_weight? : Int = 1000,
  index? : Int = 0,
) -> ServerVariant {
  {
    id: id.to_owned(),
    media: media.to_owned(),
    language: language.map(v => v.to_owned()),
    encoding: encoding.map(v => v.to_owned()),
    charset: charset.map(v => v.to_owned()),
    server_weight: clamp_quality(server_weight),
    index,
  }
}

///|
fn clamp_quality(q : Int) -> Int {
  if q < 0 {
    0
  } else if q > 1000 {
    1000
  } else {
    q
  }
}

///|
fn combine_quality(a : Int, b : Int) -> Int {
  clamp_quality(a) * clamp_quality(b) / 1000
}

///|
fn combine_all_quality(
  media : Int,
  language : Int,
  encoding : Int,
  charset : Int,
  server_weight : Int,
) -> Int {
  combine_quality(
    combine_quality(
      combine_quality(combine_quality(media, language), encoding),
      charset,
    ),
    server_weight,
  )
}

///|
fn single_media_candidate(
  prefs : RequestPreferences,
  media : String,
) -> Result[MediaCandidate?, ParseError] {
  match negotiate_media(prefs.accept[:], [media]) {
    Ok(decision) => Ok(decision.candidate)
    Err(err) => Err(err)
  }
}

///|
fn single_language_candidate(
  prefs : RequestPreferences,
  language : String?,
) -> Result[(LanguageCandidate?, Int), ParseError] {
  match language {
    Some(tag) =>
      match negotiate_language(prefs.accept_language[:], [tag]) {
        Ok(decision) =>
          match decision.candidate {
            Some(candidate) => Ok((Some(candidate), candidate.quality))
            None => Ok((None, 0))
          }
        Err(err) => Err(err)
      }
    None => Ok((None, 1000))
  }
}

///|
fn single_encoding_candidate(
  prefs : RequestPreferences,
  encoding : String?,
) -> Result[(EncodingCandidate?, Int), ParseError] {
  match encoding {
    Some(coding) =>
      match negotiate_encoding(prefs.accept_encoding[:], [coding]) {
        Ok(decision) =>
          match decision.candidate {
            Some(candidate) => Ok((Some(candidate), candidate.quality))
            None => Ok((None, 0))
          }
        Err(err) => Err(err)
      }
    None => Ok((None, 1000))
  }
}

///|
fn single_charset_candidate(
  prefs : RequestPreferences,
  charset : String?,
) -> Result[(CharsetCandidate?, Int), ParseError] {
  match charset {
    Some(value) =>
      match negotiate_charset(prefs.accept_charset[:], [value]) {
        Ok(decision) =>
          match decision.candidate {
            Some(candidate) => Ok((Some(candidate), candidate.quality))
            None => Ok((None, 0))
          }
        Err(err) => Err(err)
      }
    None => Ok((None, 1000))
  }
}

///|
fn variant_reason(
  variant : ServerVariant,
  media_q : Int,
  language_q : Int,
  encoding_q : Int,
  charset_q : Int,
  final_q : Int,
) -> String {
  let buf = StringBuilder()
  buf.write_string(variant.id)
  buf.write_string(" q=")
  buf.write_string(quality_to_string(final_q))
  buf.write_string(" from media=")
  buf.write_string(quality_to_string(media_q))
  buf.write_string(", language=")
  buf.write_string(quality_to_string(language_q))
  buf.write_string(", encoding=")
  buf.write_string(quality_to_string(encoding_q))
  buf.write_string(", charset=")
  buf.write_string(quality_to_string(charset_q))
  buf.write_string(", server=")
  buf.write_string(quality_to_string(variant.server_weight))
  buf.to_string()
}

///|
fn build_variant_candidate(
  prefs : RequestPreferences,
  variant : ServerVariant,
) -> Result[VariantCandidate?, ParseError] {
  let media = match single_media_candidate(prefs, variant.media) {
    Ok(Some(candidate)) => candidate
    Ok(None) => return Ok(None)
    Err(err) => return Err(err)
  }
  let (language, language_q) = match
    single_language_candidate(prefs, variant.language) {
    Ok(value) => value
    Err(err) => return Err(err)
  }
  if language_q == 0 {
    return Ok(None)
  }
  let (encoding, encoding_q) = match
    single_encoding_candidate(prefs, variant.encoding) {
    Ok(value) => value
    Err(err) => return Err(err)
  }
  if encoding_q == 0 {
    return Ok(None)
  }
  let (charset, charset_q) = match
    single_charset_candidate(prefs, variant.charset) {
    Ok(value) => value
    Err(err) => return Err(err)
  }
  if charset_q == 0 {
    return Ok(None)
  }
  let final_q = combine_all_quality(
    media.quality,
    language_q,
    encoding_q,
    charset_q,
    variant.server_weight,
  )
  if final_q == 0 {
    return Ok(None)
  }
  Ok(
    Some({
      variant,
      media: Some(media),
      language,
      encoding,
      charset,
      quality: final_q,
      reason: variant_reason(
        variant,
        media.quality,
        language_q,
        encoding_q,
        charset_q,
        final_q,
      ),
    }),
  )
}

///|
fn better_variant(
  candidate : VariantCandidate,
  current : VariantCandidate,
) -> Bool {
  if candidate.quality != current.quality {
    candidate.quality > current.quality
  } else if candidate.variant.server_weight != current.variant.server_weight {
    candidate.variant.server_weight > current.variant.server_weight
  } else {
    candidate.variant.index < current.variant.index
  }
}

///|
fn contains_string(values : Array[String], value : String) -> Bool {
  for item in values {
    if item == value {
      return true
    }
  }
  false
}

///|
fn push_unique(values : Array[String], value : String) -> Unit {
  if !contains_string(values, value) {
    values.push(value)
  }
}

///|
fn infer_vary(
  prefs : RequestPreferences,
  variants : Array[ServerVariant],
) -> Array[String] {
  let vary : Array[String] = []
  if !prefs.accept.is_empty() {
    push_unique(vary, "Accept")
  }
  if !prefs.accept_language.is_empty() {
    push_unique(vary, "Accept-Language")
  }
  if !prefs.accept_encoding.is_empty() {
    push_unique(vary, "Accept-Encoding")
  }
  if !prefs.accept_charset.is_empty() {
    push_unique(vary, "Accept-Charset")
  }
  let mut has_language = false
  let mut has_encoding = false
  let mut has_charset = false
  for variant in variants {
    if variant.language is Some(_) {
      has_language = true
    }
    if variant.encoding is Some(_) {
      has_encoding = true
    }
    if variant.charset is Some(_) {
      has_charset = true
    }
  }
  if has_language {
    push_unique(vary, "Accept-Language")
  }
  if has_encoding {
    push_unique(vary, "Accept-Encoding")
  }
  if has_charset {
    push_unique(vary, "Accept-Charset")
  }
  vary
}

///|
fn variant_explanation(best : VariantCandidate?) -> String {
  match best {
    Some(candidate) => {
      let buf = StringBuilder()
      buf.write_string("selected ")
      buf.write_string(candidate.variant.id)
      buf.write_string(": ")
      buf.write_string(candidate.reason)
      buf.to_string()
    }
    None => "no acceptable variant matched"
  }
}

///|
pub fn negotiate_variant(
  preferences : RequestPreferences,
  variants : Array[ServerVariant],
) -> Result[VariantDecision, ParseError] {
  let matches : Array[VariantCandidate] = []
  let mut best : VariantCandidate? = None
  for i in 0.. {
        matches.push(candidate)
        if (match best {
            None => true
            Some(current) => better_variant(candidate, current)
          }) {
          best = Some(candidate)
        }
      }
      Ok(None) => ()
      Err(err) => return Err(err)
    }
  }
  let selected = match best {
    Some(candidate) => Some(candidate.variant)
    None => None
  }
  let quality = match best {
    Some(candidate) => candidate.quality
    None => 0
  }
  Ok({
    selected,
    quality,
    matches,
    vary: infer_vary(preferences, variants),
    explanation: variant_explanation(best),
  })
}

///|
pub fn summarize_response(decision : VariantDecision) -> ResponseSummary {
  match decision.selected {
    Some(variant) =>
      {
        status: 200,
        content_type: Some(variant.media),
        content_language: variant.language,
        content_encoding: variant.encoding,
        charset: variant.charset,
        vary: decision.vary,
        explanation: decision.explanation,
      }
    None =>
      {
        status: 406,
        content_type: None,
        content_language: None,
        content_encoding: None,
        charset: None,
        vary: decision.vary,
        explanation: decision.explanation,
      }
  }
}

///|
pub fn format_variant_decision(decision : VariantDecision) -> String {
  let buf = StringBuilder()
  buf.write_string(decision.explanation)
  buf.write_string(" (matches=")
  buf.write_string(decision.matches.length().to_string())
  buf.write_string(", vary=")
  buf.write_string(join_header_names(decision.vary))
  buf.write_char(')')
  buf.to_string()
}

///|
pub fn format_response_summary(summary : ResponseSummary) -> String {
  let buf = StringBuilder()
  buf.write_string("status=")
  buf.write_string(summary.status.to_string())
  match summary.content_type {
    Some(value) => {
      buf.write_string(", content-type=")
      buf.write_string(value)
    }
    None => ()
  }
  match summary.content_language {
    Some(value) => {
      buf.write_string(", content-language=")
      buf.write_string(value)
    }
    None => ()
  }
  match summary.content_encoding {
    Some(value) => {
      buf.write_string(", content-encoding=")
      buf.write_string(value)
    }
    None => ()
  }
  match summary.charset {
    Some(value) => {
      buf.write_string(", charset=")
      buf.write_string(value)
    }
    None => ()
  }
  if summary.vary.length() > 0 {
    buf.write_string(", vary=")
    buf.write_string(join_header_names(summary.vary))
  }
  buf.write_string(", ")
  buf.write_string(summary.explanation)
  buf.to_string()
}