// serializer.mbt — Deterministic serialisation and canonicalisation.
//
// Serialising a `ContentDisposition` back to a header field value is
// deliberately deterministic:
//
//   - the disposition type is emitted in canonical lowercase
//     (`inline` / `attachment` / an extension token lower-cased);
//   - every parameter name is emitted in canonical lowercase
//     (parameter names are case-insensitive, RFC 6266 Section 4.2);
//   - parameters keep their model order;
//   - token values are emitted unquoted when they are valid tokens, quoted
//     values through `serialize_quoted_string`, and extended values through
//     `serialize_extended_value` (which re-encodes deterministically with
//     uppercase hex).
//
// `canonicalize_content_disposition` is the user-facing normaliser: it
// parses a header value and re-serialises it with one additional rule — when
// both `filename` and `filename*` are present, `filename` (the fallback)
// precedes `filename*` (the primary). Canonicalisation is idempotent:
// `canonicalize(canonicalize(x)) == canonicalize(x)`.

///|
/// Serialises a `ContentDisposition` to a header field value, deterministically.
///
/// The disposition type and every parameter name are emitted in canonical
/// lowercase; parameters keep their model order. Token values must be valid
/// tokens; a hand-built model with an invalid token value is rejected rather
/// than silently re-quoted.
///
/// Errors: `Serialization::UnexpectedCharacter` (a token value is not a
/// valid token).
pub fn serialize_content_disposition(cd : ContentDisposition) -> Result[String, DispositionError] {
  let sb = StringBuilder()
  sb.write_string(cd.disposition_type().to_lower_name())
  for p in cd.parameters() {
    match check_param_name_token(p) {
      Ok(_) => ()
      Err(e) => return Err(e)
    }
    sb.write_string("; ")
    sb.write_string(p.name().to_lower())
    sb.write_string("=")
    match p.value() {
      Token(v) => {
        if !can_be_token(v) {
          return Err(
            disposition_error(
              Serialization,
              UnexpectedCharacter,
              "token value is not a valid token: \{truncate_for_error(v)}",
            ),
          )
        }
        sb.write_string(v)
      }
      Quoted(v) => sb.write_string(serialize_quoted_string(v))
      Extended(ev) => sb.write_string(serialize_extended_value(ev))
    }
  }
  Ok(sb.to_string())
}

///|
/// Serialises a `ContentDisposition` preserving the original casing of the
/// disposition type and parameter names. Used by round-trip tests to check
/// that parsing preserves information. Not recommended for output: use
/// `serialize_content_disposition` for canonical output.
pub fn serialize_content_disposition_preserve_case(
  cd : ContentDisposition
) -> Result[String, DispositionError] {
  let sb = StringBuilder()
  match cd.raw_disposition_type() {
    Some(raw) => sb.write_string(raw)
    None =>
      match cd.disposition_type() {
        Inline => sb.write_string("inline")
        Attachment => sb.write_string("attachment")
        Extension(name) => sb.write_string(name)
      }
  }
  for p in cd.parameters() {
    match check_param_name_token(p) {
      Ok(_) => ()
      Err(e) => return Err(e)
    }
    sb.write_string("; ")
    sb.write_string(p.name())
    sb.write_string("=")
    match p.value() {
      Token(v) => {
        if !can_be_token(v) {
          return Err(
            disposition_error(
              Serialization,
              UnexpectedCharacter,
              "token value is not a valid token: \{truncate_for_error(v)}",
            ),
          )
        }
        sb.write_string(v)
      }
      Quoted(v) => sb.write_string(serialize_quoted_string(v))
      Extended(ev) => sb.write_string(serialize_extended_value(ev))
    }
  }
  Ok(sb.to_string())
}

// Validates a parameter name for serialisation, using the `Token` stage.
fn check_param_name_token(p : DispositionParameter) -> Result[Unit, DispositionError] {
  if !validate_token(p.name()) {
    return Err(
      disposition_error(
        Token,
        InvalidParameterName,
        "parameter name is not a valid token: \{truncate_for_error(p.name())}",
      ),
    )
  }
  Ok(())
}

///|
/// Parses and re-serialises a Content-Disposition header value into a
/// canonical form. The canonical form lower-cases the disposition type and
/// every parameter name, preserves parameter order except that a plain
/// `filename` parameter is emitted before a `filename*` parameter when both
/// are present, and re-encodes values deterministically. The operation is
/// idempotent: canonicalising the canonical form is a no-op.
///
/// Errors: any error that `parse_content_disposition` can raise.
pub fn canonicalize_content_disposition(input : String) -> Result[String, DispositionError] {
  let cd = match parse_content_disposition(input) {
    Ok(c) => c
    Err(e) => return Err(e)
  }
  let sb = StringBuilder()
  sb.write_string(cd.disposition_type().to_lower_name())
  for p in canonical_param_order(cd) {
    match check_param_name_token(p) {
      Ok(_) => ()
      Err(e) => return Err(e)
    }
    sb.write_string("; ")
    sb.write_string(p.name().to_lower())
    sb.write_string("=")
    match p.value() {
      Token(v) => {
        // A parsed token is always a valid token; this is defensive.
        if !can_be_token(v) {
          return Err(
            disposition_error(
              Serialization,
              UnexpectedCharacter,
              "token value is not a valid token: \{truncate_for_error(v)}",
            ),
          )
        }
        sb.write_string(v)
      }
      Quoted(v) => sb.write_string(serialize_quoted_string(v))
      Extended(ev) => sb.write_string(serialize_extended_value(ev))
    }
  }
  Ok(sb.to_string())
}

///|
/// The canonical parameter order: input order, except that every `filename*`
/// parameter is delayed until after the first plain `filename` parameter, so
/// that the fallback name precedes the primary name.
fn canonical_param_order(cd : ContentDisposition) -> Array[DispositionParameter] {
  let out : Array[DispositionParameter] = []
  let mut deferred : Array[DispositionParameter] = []
  let mut saw_filename = false
  for p in cd.parameters() {
    if p.name().equal_ignore_ascii_case("filename*") {
      if saw_filename {
        out.push(p)
      } else {
        deferred.push(p)
      }
    } else {
      out.push(p)
      if p.name().equal_ignore_ascii_case("filename") {
        saw_filename = true
        for d in deferred {
          out.push(d)
        }
        deferred = []
      }
    }
  }
  for d in deferred {
    out.push(d)
  }
  out
}

// A bounded excerpt of a value for an error message.
fn truncate_for_error(value : String) -> String {
  if value.char_length() > 40 {
    value[:40].to_owned() + "..."
  } else {
    value
  }
}