// parameter.mbt — Single disposition parameter parsing.
//
// RFC 6266 defines each disposition parameter as
//
//     disposition-parm = filename-parm / disp-ext-parm
//     filename-parm    = "filename" "=" value
//                      / "filename*" "=" ext-value
//     disp-ext-parm    = token "=" value
//                      / ext-token "=" ext-value
//
// A parameter name that ends with `*` (with a non-empty base name) carries
// an RFC 8187 extended value; every other parameter carries either a token
// or a quoted-string value. This module parses exactly one parameter,
// leaving the cursor positioned just after its value.

///|
/// Parses a single parameter starting at the current scanner position
/// (which must be the first byte of the parameter name). On success the
/// cursor is positioned after the parameter value. Compatible-mode
/// recoveries are recorded on the collector.
pub fn parse_parameter(
  cursor : Scanner,
  limits : Limits,
  mode : ParseMode,
  collector : ParseCollector
) -> Result[DispositionParameter, DispositionError] {
  try {
    Ok(parse_parameter_inner(cursor, limits, mode, collector))
  } catch {
    e => Err(unwrap_disposition_error(e))
  }
}

fn parse_parameter_inner(
  cursor : Scanner,
  limits : Limits,
  mode : ParseMode,
  collector : ParseCollector
) -> DispositionParameter raise {
  let name_start = cursor.position()
  let (ns, ne) = cursor.consume_token()
  if ne == ns {
    raise disposition_error_at(
      ParameterName,
      ExpectedToken,
      name_start,
      "expected a parameter name",
    )
  }
  if ne - ns > limits.max_parameter_name_bytes() {
    raise disposition_error_at(
      Limit,
      LimitExceeded,
      name_start,
      "parameter name exceeds max_parameter_name_bytes",
    )
  }
  let name = cursor.take_string(ns, ne)
  cursor.skip_ows()
  if !cursor.consume_char(61) {
    // '='
    raise disposition_error_at(
      ParameterName,
      MissingEquals,
      cursor.position(),
      "expected '=' after parameter name '\{name}'",
    )
  }
  cursor.skip_ows()
  let value = match parse_parameter_value(cursor, limits, mode, name, collector) {
    Ok(v) => v
    Err(e) => raise e
  }
  disposition_parameter(name, value)
}

// Parses the value half of a parameter, dispatching on the extended form.
fn parse_parameter_value(
  cursor : Scanner,
  limits : Limits,
  mode : ParseMode,
  name : String,
  collector : ParseCollector
) -> Result[ParameterValue, DispositionError] raise {
  if is_extended_param_name(name) {
    return parse_extended_value_param(cursor, limits, mode, collector)
  }
  // plain parameter value: token / quoted-string
  match cursor.peek_byte() {
    Some(b) if b == 34 => {
      // '"'
      let s = unwrap_or_raise(parse_quoted_string(cursor, limits))
      Ok(Quoted(s))
    }
    _ => {
      // unquoted run; compatible mode additionally tolerates obs-text
      // bytes for legacy non-ASCII unquoted values.
      let (vs, ve) = if mode == Compatible {
        cursor.consume_while(fn(b) { token_char(b) || is_obs_text(b) })
      } else {
        cursor.consume_token()
      }
      if ve == vs {
        if mode == Compatible {
          collector.record_recovery("empty-parameter-value")
          Ok(Quoted(""))
        } else {
          raise disposition_error_at(
            ParameterValue,
            MissingParameterValue,
            cursor.position(),
            "expected a parameter value after '='",
          )
        }
      } else {
        if ve - vs > limits.max_parameter_value_bytes() {
          raise disposition_error_at(
            Limit,
            LimitExceeded,
            vs,
            "parameter value exceeds max_parameter_value_bytes",
          )
        }
        let s = cursor.take_string(vs, ve)
        if mode == Compatible && range_has_obs_text(cursor, vs, ve) {
          collector.record_recovery("legacy-unquoted-non-ascii-value")
        }
        Ok(Token(s))
      }
    }
  }
}

// Parses the value half of a `*` parameter (an RFC 8187 extended value).
fn parse_extended_value_param(
  cursor : Scanner,
  limits : Limits,
  mode : ParseMode,
  collector : ParseCollector
) -> Result[ParameterValue, DispositionError] raise {
  match cursor.peek_byte() {
    Some(b) if b == 34 => {
      // '"'
      if mode == Strict {
        raise disposition_error_at(
          ExtendedValue,
          InvalidExtendedValue,
          cursor.position(),
          "an extended value must not be quoted (strict mode)",
        )
      }
      let content = unwrap_or_raise(parse_quoted_string(cursor, limits))
      collector.record_recovery("quoted-extended-value")
      let ev = match parse_extended_value_string(content, limits) {
        Ok(ev) => ev
        Err(e) => raise e
      }
      Ok(Extended(ev))
    }
    _ => {
      let ev = unwrap_or_raise(parse_extended_value(cursor, limits))
      Ok(Extended(ev))
    }
  }
}

///|
/// Whether a parameter name denotes an RFC 8187 extended value (a
/// non-empty base name followed by `*`).
pub fn is_extended_param_name(name : String) -> Bool {
  name.char_length() > 1 && name.has_suffix("*")
}

// Whether the byte range [start, end) of the cursor contains obs-text.
fn range_has_obs_text(cursor : Scanner, start : Int, end : Int) -> Bool {
  for i = start; i < end; i = i + 1 {
    match cursor.byte_at(i) {
      Some(b) if is_obs_text(b) => return true
      _ => ()
    }
  }
  false
}