///|
/// Token in a topic pattern.
pub(all) enum TopicToken {
  ExactSegment(String)
  OneSegment
  TailSegments
} derive(Eq, @debug.Debug)

///|
/// Parsed topic pattern. Higher specificity wins when routes are sorted.
pub(all) struct TopicPattern {
  raw : String
  tokens : Array[TopicToken]
  specificity : Int
} derive(Eq, @debug.Debug)

///|
pub(all) struct MatchReport {
  pattern : String
  topic : String
  matched : Bool
  specificity : Int
  topic_segments : Int
} derive(Eq, @debug.Debug)

///|
pub fn topic_pattern(
  pattern : StringView,
) -> Result[TopicPattern, EventRailError] {
  let raw = pattern.to_owned()
  guard raw != "" else { return Err(EmptyPattern) }
  let segments = raw.split(".").to_array()
  let tokens : Array[TopicToken] = Array::new(capacity=segments.length())
  let mut specificity = 0
  for idx, seg_view in segments {
    let seg = seg_view.to_owned()
    guard seg != "" else { return Err(EmptySegment(raw)) }
    if seg == "*" {
      tokens.push(OneSegment)
      specificity += 1
    } else if seg == "**" {
      guard idx == segments.length() - 1 else {
        return Err(InvalidPattern(raw))
      }
      tokens.push(TailSegments)
    } else if seg.contains("*") {
      return Err(InvalidPattern(raw))
    } else {
      tokens.push(ExactSegment(seg))
      specificity += 4
    }
  }
  Ok({ raw, tokens, specificity })
}

///|
pub fn topic_segments(
  topic : StringView,
) -> Result[Array[String], EventRailError] {
  let raw = topic.to_owned()
  guard raw != "" else { return Err(EmptyTopic) }
  guard !raw.contains("*") else { return Err(InvalidPattern(raw)) }
  let parts = raw.split(".").to_array()
  let out : Array[String] = Array::new(capacity=parts.length())
  for part in parts {
    let owned = part.to_owned()
    guard owned != "" else { return Err(EmptySegment(raw)) }
    out.push(owned)
  }
  Ok(out)
}

///|
pub fn TopicPattern::matches_topic(
  self : TopicPattern,
  topic : StringView,
) -> Result[Bool, EventRailError] {
  match topic_segments(topic) {
    Err(err) => Err(err)
    Ok(segments) => Ok(match_token_at(self.tokens, segments, 0, 0))
  }
}

///|
pub fn TopicPattern::report(
  self : TopicPattern,
  topic : StringView,
) -> Result[MatchReport, EventRailError] {
  match topic_segments(topic) {
    Err(err) => Err(err)
    Ok(segments) =>
      Ok({
        pattern: self.raw,
        topic: topic.to_owned(),
        matched: match_token_at(self.tokens, segments, 0, 0),
        specificity: self.specificity,
        topic_segments: segments.length(),
      })
  }
}

///|
pub fn TopicPattern::is_catch_all(self : TopicPattern) -> Bool {
  self.tokens.length() == 1 && self.tokens[0] is TailSegments
}

///|
pub fn TopicPattern::prefix(self : TopicPattern) -> String {
  let parts : Array[String] = []
  for token in self.tokens {
    match token {
      ExactSegment(value) => parts.push(value)
      OneSegment | TailSegments => break
    }
  }
  parts.join(".")
}

///|
fn match_token_at(
  tokens : Array[TopicToken],
  segments : Array[String],
  token_idx : Int,
  segment_idx : Int,
) -> Bool {
  if token_idx == tokens.length() {
    return segment_idx == segments.length()
  }
  match tokens[token_idx] {
    ExactSegment(expected) =>
      segment_idx < segments.length() &&
      segments[segment_idx] == expected &&
      match_token_at(tokens, segments, token_idx + 1, segment_idx + 1)
    OneSegment =>
      segment_idx < segments.length() &&
      match_token_at(tokens, segments, token_idx + 1, segment_idx + 1)
    TailSegments => true
  }
}

///|
pub fn compare_pattern_priority(
  left : TopicPattern,
  right : TopicPattern,
) -> Int {
  let score = right.specificity - left.specificity
  if score != 0 {
    score
  } else {
    left.raw.compare(right.raw)
  }
}