// rfc8187.mbt — RFC 8187 extended parameter values.
//
// RFC 8187 defines the `ext-value` microsyntax used by the `title*` and
// `name*` parameters of RFC 8288:
//
//     ext-value   = charset  "'" [ language ] "'" value-chars
//     charset     = "UTF-8" / mime-charset
//     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 (case-insensitively) and rejects
// every other charset with `UnsupportedCharset` rather than decoding it
// incorrectly. The decoded value is stored as a MoonBit String; the
// charset is always reported as "UTF-8" on serialisation.

///|
/// Parses an extended value starting at the current scanner position.
/// On success the cursor is positioned immediately after the last
/// value-char. The value part must not contain whitespace, `;`, or `,`;
/// those terminate the value.
///
/// Errors: `ExtendedValue::UnexpectedCharacter` (malformed charset or
/// separator), `ExtendedValue::InvalidPercentEncoding`,
/// `ExtendedValue::InvalidUtf8` (percent-decoded bytes are not valid
/// UTF-8), `ExtendedValue::UnsupportedCharset`, and
/// `ExtendedValue::InvalidLanguageTag`.
pub fn parse_extended_value(
  cursor : Scanner,
  limits : Limits,
) -> ExtendedValue 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 link_error_at(
      ExtendedValue,
      UnexpectedCharacter,
      start,
      "expected charset before the first apostrophe",
    )
  }
  if !cursor.consume_char(39) {
    raise link_error_at(
      ExtendedValue,
      UnexpectedCharacter,
      cursor.position(),
      "expected apostrophe after charset",
    )
  }
  let charset = 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 link_error_at(
      ExtendedValue,
      UnexpectedCharacter,
      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 link_error_at(
        ExtendedValue,
        InvalidLanguageTag,
        lang_start,
        "invalid Language-Tag: \{lang_raw}",
      )
    }
    Some(lang_raw)
  }

  // value-chars: *( pct-encoded / attr-char )
  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 link_error_at(
              ExtendedValue,
              InvalidPercentEncoding,
              pct_pos,
              "expected '%' followed by two hex digits",
            )
        }
      }
      _ => done = true
    }
    if cursor.position() - start > limits.max_parameter_value_bytes() {
      raise link_error_at(
        Limit,
        LimitExceeded,
        cursor.position(),
        "extended value exceeds max_parameter_value_bytes",
      )
    }
  }

  // charset support check + UTF-8 decode
  if !charset.equal_ignore_ascii_case("UTF-8") {
    raise link_error_at(
      ExtendedValue,
      UnsupportedCharset,
      cs_start,
      "unsupported charset: \{charset} (only UTF-8 is implemented)",
    )
  }
  let encoded = Bytes::from_array(value_bytes)
  let value = @utf8.decode(encoded.view(start=0, end=encoded.length())) catch {
    _ =>
      raise link_error_at(
        ExtendedValue,
        InvalidUtf8,
        start,
        "percent-decoded value is not valid UTF-8",
      )
  }
  { charset: "UTF-8", language, value }
}

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

///|
/// Serialises an extended value deterministically. The charset is always
/// emitted as `UTF-8`; 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 sb = StringBuilder()
  sb.write_string("UTF-8")
  sb.write_char('\'')
  match ev.language {
    Some(l) => sb.write_string(l)
    None => ()
  }
  sb.write_char('\'')
  let bytes = @utf8.encode(ev.value)
  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 `*`, `'`, `%`.
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 subtag (RFC 5646: ALPHA /
/// DIGIT; we check this per character and validate structure 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:
/// one or more subtags separated by `-`; the primary subtag is 2-8
/// letters (or `x` for private use); each extended subtag is 1-8
/// alphanumeric characters.
pub fn valid_language_tag(tag : String) -> Bool {
  if tag.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
  if flen < 2 || flen > 8 {
    // The only legal one-letter primary subtag is the private-use prefix "x"
    // (RFC 5646 `privateuse`); every other primary subtag is 2-8 letters.
    if flen == 1 && bytes[fs] == 120 {
      ()
    } else {
      return false
    }
  }
  // primary subtag must be all letters, or the single letter "x"
  if !all_alpha(bytes, fs, fe) {
    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
}

///|
/// Numeric value of a hex digit byte.
fn hex_value(b : Byte) -> Int {
  let v = b.to_int()
  if v >= 48 && v <= 57 {
    v - 48
  } else if v >= 65 && v <= 70 {
    v - 55
  } else if v >= 97 && v <= 102 {
    v - 87
  } else {
    0
  }
}

///|
/// Uppercase hex digit character for a nibble (0-15).
fn hex_char(nibble : Int) -> Char {
  let v = nibble & 0xF
  if v < 10 {
    (48 + v).unsafe_to_char()
  } else {
    (65 + v - 10).unsafe_to_char()
  }
}