// model.mbt — Core data model for moon-content-disposition.
//
// The model follows the abstract model of RFC 6266 Section 4: a
// Content-Disposition header field value is a disposition type followed by
// zero or more disposition parameters. The model keeps every parsed
// parameter (including extension parameters and duplicates) in input order
// so that no information is lost between parsing and serialising. Typed
// convenience accessors (`filename()`, `filename_star()`,
// `preferred_filename()`) are layered on top of the ordered raw model rather
// than replacing it.
//
// Design notes:
//   - Parameter names are compared case-insensitively (RFC 6266
//     Section 4.2) but stored exactly as parsed so that serialising a
//     parsed header preserves the original casing until canonicalisation.
//   - `ParameterValue::Token` and `ParameterValue::Quoted` are kept
//     distinct: the RFC 6266 `value` rule is `token / quoted-string`, and
//     keeping the two forms apart lets the serializer emit the same wire
//     form it received (deterministic round-tripping).
//   - `ParameterValue::Extended` carries the RFC 8187 extended value used
//     by `filename*` and any extension parameter whose name ends with `*`.
//   - `ContentDisposition` is a value type: it never mutates in place.

///|
/// The disposition type of a Content-Disposition field value.
///
/// `Inline` and `Attachment` are the two registered disposition types
/// (RFC 6266 Section 4.1); anything else is a valid extension token
/// (`disp-ext-type`), preserved verbatim.
pub enum DispositionType {
  Inline
  Attachment
  Extension(String)
}

///|
/// The value of a single disposition parameter.
///
/// `Token` is the unquoted `token` form, `Quoted` is the `quoted-string`
/// form, and `Extended` is an RFC 8187 extended value (used when the
/// parameter name ends with `*`).
pub enum ParameterValue {
  Token(String)
  Quoted(String)
  Extended(ExtendedValue)
}

///|
/// An RFC 8187 extended value: a charset name, an optional language tag,
/// and the decoded value. The charset is always a supported charset
/// (`UTF-8` or `ISO-8859-1`, stored in canonical casing) after validation;
/// the value is the fully decoded string.
pub struct ExtendedValue {
  charset : String
  language : String?
  value : String
}

///|
/// A single disposition parameter: a name and a value.
pub struct DispositionParameter {
  name : String
  value : ParameterValue
}

///|
/// A parsed Content-Disposition header field value.
///
/// `parameters` preserves the input order of every parameter, including
/// extension parameters and (in compatible mode) duplicates. The typed
/// accessors below look parameters up without consuming them.
pub struct ContentDisposition {
  disposition_type : DispositionType
  parameters : Array[DispositionParameter]
  raw_disposition_type : String?
}

///|
/// The single library version string, used by `moon --version`-style
/// reporting, the CLI and the module metadata. Kept in one place so the
/// final maintainer can update it (together with `moon.mod`) in one step.
pub fn library_version() -> String {
  "0.1.1"
}

///|
/// Constructs an `ExtendedValue` from its parts.
pub fn extended_value(
  charset : String,
  language : String?,
  value : String
) -> ExtendedValue {
  { charset, language, value }
}

///|
/// Constructs a `DispositionParameter` from its parts.
pub fn disposition_parameter(name : String, value : ParameterValue) -> DispositionParameter {
  { name, value }
}

///|
/// Constructs a `ContentDisposition` with the given disposition type and
/// no parameters. Hand-built values have no raw disposition type; the
/// preserve-case serializer falls back to the canonical spelling.
pub fn content_disposition(disposition_type : DispositionType) -> ContentDisposition {
  { disposition_type, parameters: Array::new(), raw_disposition_type: None }
}

///|
/// Constructs a `ContentDisposition` while remembering the exact disposition
/// type token as it appeared on the wire, so that a preserve-case
/// serialisation can emit it verbatim. Used by the parser.
pub fn content_disposition_with_raw(
  disposition_type : DispositionType,
  raw : String
) -> ContentDisposition {
  { disposition_type, parameters: Array::new(), raw_disposition_type: Some(raw) }
}

///|
/// The disposition type.
pub fn ContentDisposition::disposition_type(self : ContentDisposition) -> DispositionType {
  self.disposition_type
}

///|
/// The disposition type token exactly as it appeared in the input, if this
/// value was produced by parsing (never present on hand-built models).
pub fn ContentDisposition::raw_disposition_type(self : ContentDisposition) -> String? {
  self.raw_disposition_type
}

///|
/// All parameters, in input order.
pub fn ContentDisposition::parameters(self : ContentDisposition) -> Array[DispositionParameter] {
  self.parameters
}

///|
/// The number of parameters.
pub fn ContentDisposition::parameter_count(self : ContentDisposition) -> Int {
  self.parameters.length()
}

///|
/// The parameter with the given name (case-insensitive), or `None`. If the
/// name appears more than once, the first occurrence is returned.
pub fn ContentDisposition::get_parameter(
  self : ContentDisposition,
  name : String
) -> DispositionParameter? {
  for p in self.parameters {
    if p.name.equal_ignore_ascii_case(name) {
      return Some(p)
    }
  }
  None
}

///|
/// The disposition type token, lower-cased for comparison.
pub fn DispositionType::to_lower_name(self : DispositionType) -> String {
  match self {
    Inline => "inline"
    Attachment => "attachment"
    Extension(name) => name.to_lower()
  }
}

///|
/// Whether the disposition type is the registered `inline` type.
pub fn DispositionType::is_inline(self : DispositionType) -> Bool {
  match self {
    Inline => true
    _ => false
  }
}

///|
/// Whether the disposition type is the registered `attachment` type.
pub fn DispositionType::is_attachment(self : DispositionType) -> Bool {
  match self {
    Attachment => true
    _ => false
  }
}

///|
/// Whether the disposition type is an extension token.
pub fn DispositionType::is_extension(self : DispositionType) -> Bool {
  match self {
    Extension(_) => true
    _ => false
  }
}

///|
/// The extension token, if this is an extension disposition type.
pub fn DispositionType::extension_name(self : DispositionType) -> String? {
  match self {
    Extension(name) => Some(name)
    _ => None
  }
}

///|
/// The parameter name.
pub fn DispositionParameter::name(self : DispositionParameter) -> String {
  self.name
}

///|
/// The parameter value.
pub fn DispositionParameter::value(self : DispositionParameter) -> ParameterValue {
  self.value
}

///|
/// Whether the parameter name ends with `*` (the RFC 8187 extended form).
pub fn DispositionParameter::is_extended(self : DispositionParameter) -> Bool {
  self.name.has_suffix("*")
}

///|
/// The plain string content of a non-extended parameter value, if this
/// value is not the extended form.
pub fn ParameterValue::plain(self : ParameterValue) -> String? {
  match self {
    Token(s) => Some(s)
    Quoted(s) => Some(s)
    Extended(_) => None
  }
}

///|
/// The extended value, if this is the extended form.
pub fn ParameterValue::extended(self : ParameterValue) -> ExtendedValue? {
  match self {
    Extended(ev) => Some(ev)
    _ => None
  }
}

///|
/// Whether this value is the RFC 8187 extended form.
pub fn ParameterValue::is_extended(self : ParameterValue) -> Bool {
  match self {
    Extended(_) => true
    _ => false
  }
}

///|
/// The charset of this extended value.
pub fn ExtendedValue::charset(self : ExtendedValue) -> String {
  self.charset
}

///|
/// The optional language tag of this extended value.
pub fn ExtendedValue::language(self : ExtendedValue) -> String? {
  self.language
}

///|
/// The decoded value of this extended value.
pub fn ExtendedValue::value(self : ExtendedValue) -> String {
  self.value
}

///|
/// The value of the last `filename` parameter, if present. Only the plain
/// (non-extended) `filename` parameter is considered; the decoded value of
/// `filename*` is exposed through `filename_star()`.
pub fn ContentDisposition::filename(self : ContentDisposition) -> String? {
  let mut result : String? = None
  for p in self.parameters {
    if p.name.equal_ignore_ascii_case("filename") {
      match p.value.plain() {
        Some(v) => result = Some(v)
        None => ()
      }
    }
  }
  result
}

///|
/// The decoded extended value of the last `filename*` parameter, if present.
pub fn ContentDisposition::filename_star(self : ContentDisposition) -> ExtendedValue? {
  let mut result : ExtendedValue? = None
  for p in self.parameters {
    if p.name.equal_ignore_ascii_case("filename*") {
      match p.value {
        Extended(ev) => result = Some(ev)
        _ => ()
      }
    }
  }
  result
}

///|
/// Whether this disposition type matches the given token, case-insensitively.
pub fn DispositionType::matches(self : DispositionType, name : String) -> Bool {
  match self {
    Inline => name.equal_ignore_ascii_case("inline")
    Attachment => name.equal_ignore_ascii_case("attachment")
    Extension(n) => n.equal_ignore_ascii_case(name)
  }
}

///|
/// Semantic equality between two extended values: charset, language and
/// value. Used by round-trip tests.
pub fn ExtendedValue::semantic_equal(self : ExtendedValue, other : ExtendedValue) -> Bool {
  self.charset.equal_ignore_ascii_case(other.charset) &&
    self.language == other.language && self.value == other.value
}

///|
/// Semantic equality between two parameter values. Token and Quoted forms
/// of the same string are considered different forms but semantically equal
/// content; extended values are compared structurally.
pub fn ParameterValue::semantic_equal(self : ParameterValue, other : ParameterValue) -> Bool {
  match (self, other) {
    (Token(a), Token(b)) => a == b
    (Quoted(a), Quoted(b)) => a == b
    (Token(a), Quoted(b)) => a == b
    (Quoted(a), Token(b)) => a == b
    (Extended(a), Extended(b)) => a.semantic_equal(b)
    _ => false
  }
}

///|
/// Semantic equality between two parameters: names are compared
/// case-insensitively, values structurally.
pub fn DispositionParameter::semantic_equal(
  self : DispositionParameter,
  other : DispositionParameter
) -> Bool {
  self.name.equal_ignore_ascii_case(other.name) && self.value.semantic_equal(other.value)
}

///|
/// Semantic equality between two Content-Disposition values. Parameter
/// order must match; names are compared case-insensitively.
pub fn ContentDisposition::semantic_equal(
  self : ContentDisposition,
  other : ContentDisposition
) -> Bool {
  if !self.disposition_type.to_lower_name().equal_ignore_ascii_case(
    other.disposition_type.to_lower_name(),
  ) {
    return false
  }
  if self.parameters.length() != other.parameters.length() {
    return false
  }
  for i = 0; i < self.parameters.length(); i = i + 1 {
    if !self.parameters[i].semantic_equal(other.parameters[i]) {
      return false
    }
  }
  true
}

///|
/// A compact human-readable rendering used by tests and the CLI.
pub fn ContentDisposition::to_debug_string(self : ContentDisposition) -> String {
  let sb = StringBuilder()
  sb.write_string(self.disposition_type.to_lower_name())
  for p in self.parameters {
    sb.write_string("; ")
    sb.write_string(p.name)
    sb.write_string("=")
    match p.value {
      Token(v) => sb.write_string(v)
      Quoted(v) => sb.write_string(serialize_quoted_string(v))
      Extended(ev) => {
        sb.write_string(ev.charset)
        sb.write_string("'")
        match ev.language {
          Some(l) => sb.write_string(l)
          None => ()
        }
        sb.write_string("'")
        sb.write_string(ev.value)
      }
    }
  }
  sb.to_string()
}