///|
/// A group of signals sharing one hierarchical scope.
pub struct SignalGroup {
  path : String
  signals : Array[Signal]
} derive(Debug)

///|
/// Return path components for a signal.
pub fn signal_path_parts(signal : Signal) -> Array[String] {
  let parts = signal.scope.copy()
  parts.push(signal.name)
  parts
}

///|
/// Return the leaf name of a signal path.
pub fn signal_leaf_name(signal : Signal) -> String {
  signal.name
}

///|
/// Return the nearest parent scope, or an empty string for a root signal.
pub fn signal_parent_path(signal : Signal) -> String {
  signal.scope.join(".")
}

///|
/// Return the number of hierarchy levels used by a signal.
pub fn signal_depth(signal : Signal) -> Int {
  signal.scope.length()
}

///|
/// Return whether two declarations live in the same scope.
pub fn same_signal_scope(left : Signal, right : Signal) -> Bool {
  left.scope == right.scope
}

///|
/// Group declarations by their exact parent scope.
pub fn signal_groups(trace : Trace) -> Array[SignalGroup] {
  let result : Array[SignalGroup] = []
  for signal in trace.signals {
    let path = signal_parent_path(signal)
    match result.search_by(fn(group) { group.path == path }) {
      Some(index) => {
        let group = result[index]
        group.signals.push(signal)
      }
      None => result.push({ path, signals: [signal] })
    }
  }
  result
}

///|
/// Return all unique scope paths from shallow to deep order.
pub fn scope_paths(trace : Trace) -> Array[String] {
  let result : Array[String] = []
  for signal in trace.signals {
    let current : Array[String] = []
    for part in signal.scope {
      current.push(part)
      let path = current.join(".")
      if !result.contains(path) {
        result.push(path)
      }
    }
  }
  result
}

///|
/// Return signals declared directly in a scope.
pub fn signals_in_scope(trace : Trace, scope : String) -> Array[Signal] {
  trace.signals.filter(fn(signal) { signal_parent_path(signal) == scope })
}

///|
/// Return signals whose fully-qualified path matches a small glob language.
pub fn find_signals_glob(trace : Trace, pattern : String) -> Array[Signal] {
  trace.signals.filter(fn(signal) { glob_matches(pattern, signal_path(signal)) })
}

///|
/// Return signal paths containing a case-insensitive substring.
pub fn find_signal_names(trace : Trace, needle : String) -> Array[Signal] {
  let query = needle.to_lower()
  trace.signals.filter(fn(signal) {
    signal_path(signal).to_lower().contains(query)
  })
}

///|
/// Return the deepest declared scope depth.
pub fn maximum_scope_depth(trace : Trace) -> Int {
  trace.signals.fold(init=0, fn(depth, signal) {
    Int::max(depth, signal_depth(signal))
  })
}

///|
/// Return one-line hierarchy summary rows.
pub fn hierarchy_report(trace : Trace) -> String {
  let out : Array[String] = []
  for path in scope_paths(trace) {
    out.push(
      path +
      ": " +
      signals_in_scope(trace, path).length().to_string() +
      " signals",
    )
  }
  if out.length() == 0 {
    ""
  } else {
    out.join("\n")
  }
}

///|
/// Match `*` against any number of characters and `?` against one character.
pub fn glob_matches(pattern : String, text : String) -> Bool {
  glob_match_at(pattern.to_array(), 0, text.to_array(), 0)
}

///|
fn glob_match_at(
  pattern : Array[Char],
  pi : Int,
  text : Array[Char],
  ti : Int,
) -> Bool {
  if pi == pattern.length() {
    return ti == text.length()
  }
  if pattern[pi] == '*' {
    if glob_match_at(pattern, pi + 1, text, ti) {
      return true
    }
    if ti < text.length() {
      return glob_match_at(pattern, pi, text, ti + 1)
    }
    false
  } else if ti == text.length() {
    false
  } else if pattern[pi] == '?' || pattern[pi] == text[ti] {
    glob_match_at(pattern, pi + 1, text, ti + 1)
  } else {
    false
  }
}

///|
/// Return a stable table of path, width, and declaration kind.
pub fn signal_catalog(trace : Trace) -> Array[(String, Int, String)] {
  trace.signals.map(fn(signal) {
    (signal_path(signal), signal.width, signal.kind)
  })
}

///|
/// Return the widest declaration in a trace.
pub fn widest_signal(trace : Trace) -> Signal? {
  let mut result : Signal? = None
  for signal in trace.signals {
    match result {
      Some(current) if current.width >= signal.width => ()
      _ => result = Some(signal)
    }
  }
  result
}