///|
/// Convert a character to the UTF-16 code unit `String` indexing yields.
fn u16(ch : Char) -> UInt16 {
  ch.to_int().to_uint16()
}

///|
/// Validate a NATS subject.
///
/// A subject is dot-separated tokens of printable ASCII characters without
/// spaces. Tokens must be non-empty, so leading/trailing dots and `..` are
/// invalid.
///
/// When `wildcards` is false (publish subjects) neither `*` nor `>` may
/// appear. When true, `*` must form an entire token and `>` must be the
/// final token.
pub fn is_valid_subject(subject : String, wildcards? : Bool = false) -> Bool {
  if subject.is_empty() {
    return false
  }
  let len = subject.length()
  let mut i = 0
  let mut token_len = 0
  while i < len {
    let ch = subject[i]
    if ch == u16('.') {
      if token_len == 0 {
        return false
      }
      token_len = 0
    } else if ch == u16('*') {
      if !wildcards || token_len != 0 {
        return false
      }
      token_len = 1
    } else if ch == u16('>') {
      if !wildcards || token_len != 0 || i + 1 != len {
        return false
      }
      token_len = 1
    } else if ch < u16('!') || ch > u16('~') {
      return false
    } else {
      token_len += 1
    }
    i += 1
  }
  token_len > 0
}

///|
/// Report whether a subscription subject (wildcards allowed) matches a
/// concrete publish subject (wildcards forbidden).
///
/// `*` matches exactly one token; a trailing `>` matches one or more
/// remaining tokens, so `foo.>` does not match `foo`. Neither input is
/// validated here; pair with `is_valid_subject` at the API boundary.
pub fn subject_matches(subscription : String, subject : String) -> Bool {
  match_tokens(subscription, 0, subject, 0)
}

///|
fn match_tokens(
  subscription : String,
  sub_start : Int,
  subject : String,
  subj_start : Int,
) -> Bool {
  let mut s = sub_start
  let mut j = subj_start
  while true {
    let sub_end = token_end(subscription, s)
    let sub_len = subscription.length()
    if is_wildcard_token(subscription, s, sub_end, '>') {
      // A valid '>' is the last subscription token and must find at least
      // one remaining subject token to absorb.
      return sub_end == sub_len && j < subject.length()
    }
    if j >= subject.length() {
      // Subject exhausted while the subscription still has a concrete token.
      return false
    }
    let subj_end = token_end(subject, j)
    let matched = if is_wildcard_token(subscription, s, sub_end, '*') {
      true
    } else {
      sub_end - s == subj_end - j &&
      token_equal(subscription, s, sub_end, subject, j)
    }
    if !matched {
      return false
    }
    s = sub_end + 1
    j = subj_end + 1
    if sub_end >= sub_len {
      // Subscription exhausted: match only if the subject has no token left.
      return j > subject.length()
    }
  }
  false
}

///|
fn is_wildcard_token(
  s : String,
  start : Int,
  end : Int,
  wildcard : Char,
) -> Bool {
  end - start == 1 && s[start] == u16(wildcard)
}

///|
/// End index (exclusive) of the token starting at `start`, i.e. the position
/// of the next '.' or the string length.
fn token_end(s : String, start : Int) -> Int {
  let mut i = start
  while i < s.length() && s[i] != u16('.') {
    i += 1
  }
  i
}

///|
fn token_equal(
  a : String,
  a_start : Int,
  a_end : Int,
  b : String,
  b_start : Int,
) -> Bool {
  let mut i = a_start
  let mut k = b_start
  while i < a_end {
    if a[i] != b[k] {
      return false
    }
    i += 1
    k += 1
  }
  true
}