// rfc8187.mbt — RFC 8187 extended parameter values.
//
// RFC 8187 defines the `ext-value` microsyntax used by the `filename*`
// parameter (and any extension parameter whose name ends with `*`):
//
//     ext-value   = charset  "'" [ language ] "'" value-chars
//     charset     = "UTF-8" / mime-charsetc
//     language    = 
//     value-chars = *( pct-encoded / attr-char )
//     pct-encoded = "%" HEXDIG HEXDIG
//
// RFC 8187 Section 3 requires implementations to support the UTF-8
// charset. This library supports UTF-8 and ISO-8859-1 (see charset.mbt)
// and rejects every other charset with `UnsupportedCharset` rather than
// decoding it incorrectly. The decoded value is stored as a MoonBit
// String; the charset is stored in canonical form.

///|
/// Parses an extended value starting at the current scanner position. On
/// success the cursor is positioned immediately after the last value-char.
///
/// Errors: `ExtendedValue::MissingCharset` (no charset before the first
/// apostrophe), `ExtendedValue::InvalidCharset` (malformed charset),
/// `ExtendedValue::InvalidExtendedValue` (missing separator),
/// `ExtendedValue::InvalidLanguage`, `ExtendedValue::InvalidPercentEncoding`,
/// `ExtendedValue::InvalidUtf8`, `Charset::UnsupportedCharset`, and
/// `Limit::LimitExceeded`.
pub fn parse_extended_value(
  cursor : Scanner,
  limits : Limits
) -> Result[ExtendedValue, DispositionError] raise {
  let start = cursor.position()

  // charset: 1*mime-charsetc
  let (cs_start, cs_end) = cursor.consume_while(fn(b) { mime_charsetc(b) })
  if cs_end == cs_start {
    raise disposition_error_at(
      ExtendedValue,
      MissingCharset,
      start,
      "expected charset before the first apostrophe",
    )
  }
  if !cursor.consume_char(39) {
    raise disposition_error_at(
      ExtendedValue,
      InvalidCharset,
      cursor.position(),
      "expected apostrophe after charset",
    )
  }
  let charset_raw = cursor.take_string(cs_start, cs_end)

  // language: [ Language-Tag ]  (may be empty)
  let (lang_start, lang_end) = cursor.consume_while(fn(b) { language_subtag_char(b) })
  if !cursor.consume_char(39) {
    raise disposition_error_at(
      ExtendedValue,
      InvalidExtendedValue,
      cursor.position(),
      "expected apostrophe after language",
    )
  }
  let lang_raw = cursor.take_string(lang_start, lang_end)
  let language : String? = if lang_raw == "" {
    None
  } else {
    if !valid_language_tag(lang_raw) {
      raise disposition_error_at(
        ExtendedValue,
        InvalidLanguage,
        lang_start,
        "invalid Language-Tag: \{lang_raw}",
      )
    }
    Some(lang_raw)
  }

  // value-chars: *( pct-encoded / attr-char )
  // The limit applies to the value-chars payload, not to the charset and
  // language prefix, so a value of exactly max_extended_value_bytes bytes
  // is accepted regardless of how long the charset name is.
  let value_start = cursor.position()
  let value_bytes : Array[Byte] = []
  let mut done = false
  while !done {
    match cursor.peek_byte() {
      Some(b) if attr_char(b) => {
        value_bytes.push(b)
        cursor.pos = cursor.pos + 1
      }
      Some(b) if b == 37 => {
        // percent-encoded octet
        let pct_pos = cursor.position()
        cursor.pos = cursor.pos + 1
        match (cursor.peek_byte(), cursor.peek_at(1)) {
          (Some(h1), Some(h2)) if is_hexdigit(h1) && is_hexdigit(h2) => {
            value_bytes.push((hex_value(h1) * 16 + hex_value(h2)).to_byte())
            cursor.pos = cursor.pos + 2
          }
          _ =>
            raise disposition_error_at(
              ExtendedValue,
              InvalidPercentEncoding,
              pct_pos,
              "expected '%' followed by two hex digits",
            )
        }
      }
      _ => done = true
    }
    if cursor.position() - value_start > limits.max_extended_value_bytes() {
      raise disposition_error_at(
        Limit,
        LimitExceeded,
        cursor.position(),
        "extended value exceeds max_extended_value_bytes",
      )
    }
  }

  // charset support check + decode
  let charset = match canonical_charset(charset_raw) {
    Some(c) => c
    None =>
      raise disposition_error_at(
        ExtendedValue,
        UnsupportedCharset,
        cs_start,
        "unsupported charset: \{charset_raw} (supported: UTF-8, ISO-8859-1)",
      )
  }
  let encoded = Bytes::from_array(value_bytes)
  let decoded = match decode_bytes(charset, encoded) {
    Ok(v) => v
    Err(e) => raise e
  }
  Ok({ charset, language, value: decoded })
}

///|
/// Parses a complete extended value from a string (the value of a `*`
/// parameter, without the name or `=`).
pub fn parse_extended_value_string(
  input : String,
  limits : Limits
) -> Result[ExtendedValue, DispositionError] {
  try {
    let cursor = Scanner::new(input)
    let ev = unwrap_or_raise(parse_extended_value(cursor, limits))
    if !cursor.eof() {
      raise disposition_error_at(
        ExtendedValue,
        TrailingInput,
        cursor.position(),
        "trailing bytes after extended value",
      )
    }
    Ok(ev)
  } catch {
    e => Err(unwrap_disposition_error(e))
  }
}

///|
/// Parses a complete extended value from a string with default limits.
pub fn parse_extended_value_default(input : String) -> Result[ExtendedValue, DispositionError] {
  parse_extended_value_string(input, Limits::default())
}

///|
/// Serialises an extended value deterministically. The charset is emitted
/// in canonical form (`UTF-8`, or `ISO-8859-1` when the value is entirely
/// representable in Latin-1); the language is emitted when present; every
/// value byte outside `attr-char` is percent-encoded with uppercase hex
/// digits.
pub fn serialize_extended_value(ev : ExtendedValue) -> String {
  let effective = if ev.charset.equal_ignore_ascii_case("ISO-8859-1") &&
    fits_iso8859_1(ev.value) {
    "ISO-8859-1"
  } else {
    "UTF-8"
  }
  let bytes = match encode_to_bytes(effective, ev.value) {
    Ok(b) => b
    Err(_) => @utf8.encode(ev.value)
  }
  let sb = StringBuilder()
  sb.write_string(effective)
  sb.write_char('\'')
  match ev.language {
    Some(l) => sb.write_string(l)
    None => ()
  }
  sb.write_char('\'')
  for i = 0; i < bytes.length(); i = i + 1 {
    let b = bytes[i]
    if attr_char(b) {
      sb.write_char(b.to_char())
    } else {
      sb.write_char('%')
      sb.write_char(hex_char(b.to_int() >> 4))
      sb.write_char(hex_char(b.to_int() & 0xF))
    }
  }
  sb.to_string()
}

///|
/// RFC 8187 `mime-charsetc`.
fn mime_charsetc(b : Byte) -> Bool {
  is_alpha(b) || is_digit(b) || b == 33 || b == 35 || b == 36 || b == 37 ||
    b == 38 || b == 43 || b == 45 || b == 94 || b == 95 || b == 96 || b == 123 ||
    b == 125 || b == 126
}

///|
/// RFC 8187 `attr-char`: token except `*`, `'`, `%`.
pub fn attr_char(b : Byte) -> Bool {
  is_alpha(b) || is_digit(b) || b == 33 || b == 35 || b == 36 || b == 38 ||
    b == 43 || b == 45 || b == 46 || b == 94 || b == 95 || b == 96 || b == 124 ||
    b == 126
}

///|
/// Characters permitted inside a language tag run (RFC 5646: ALPHA / DIGIT
/// / `-`; structural validation happens separately).
fn language_subtag_char(b : Byte) -> Bool {
  is_alpha(b) || is_digit(b) || b == 45
}

///|
/// Validates an RFC 5646 Language-Tag. Implements the pragmatic subset
/// used by RFC 8187 recipients: one or more subtags separated by `-`; the
/// primary subtag is 2-8 letters (or the single letter `x` for private
/// use); each extended subtag is 1-8 alphanumeric characters.
pub fn valid_language_tag(tag : String) -> Bool {
  if tag.char_length() == 0 {
    return false
  }
  let bytes = @utf8.encode(tag)
  // split into subtags
  let subtags : Array[(Int, Int)] = []
  let mut i = 0
  let mut sub_start = 0
  while i <= bytes.length() {
    if i == bytes.length() || bytes[i] == 45 {
      subtags.push((sub_start, i))
      sub_start = i + 1
    }
    i = i + 1
  }
  if subtags.is_empty() {
    return false
  }
  let (fs, fe) = subtags[0]
  let flen = fe - fs
  let primary_ok = if flen == 1 {
    // RFC 5646 private use: the primary subtag is the single letter x/X
    // (e.g. "x-private").
    let b = bytes[fs]
    b == 120 || b == 88
  } else {
    flen >= 2 && flen <= 8 && all_alpha(bytes, fs, fe)
  }
  if !primary_ok {
    return false
  }
  for j = 1; j < subtags.length(); j = j + 1 {
    let (s, e) = subtags[j]
    let l = e - s
    if l < 1 || l > 8 {
      return false
    }
    // every extended subtag must be alphanumeric
    for k = s; k < e; k = k + 1 {
      if !(is_alpha(bytes[k]) || is_digit(bytes[k])) {
        return false
      }
    }
  }
  true
}

fn all_alpha(bytes : Bytes, start : Int, end : Int) -> Bool {
  for i = start; i < end; i = i + 1 {
    if !is_alpha(bytes[i]) {
      return false
    }
  }
  true
}