///|
/// Static-analysis result associated with one named rule definition.
pub(all) struct RuleProgramAnalysis {
  name : String
  severity : Severity
  expression : String
  analysis : ProgramAnalysis
} derive(Eq, Debug, ToJson, FromJson)

///|
/// Aggregate static information for a compiled rule set.
pub(all) struct RuleSetAnalysis {
  rule_count : Int
  total_nodes : Int
  maximum_ast_depth : Int
  estimated_cost : Int
  warning_count : Int
  note_count : Int
  called_functions : Array[String]
  referenced_paths : Array[String]
  rules : Array[RuleProgramAnalysis]
} derive(Eq, Debug, ToJson, FromJson)

///|
pub fn RuleSetAnalysis::to_json_string(
  self : RuleSetAnalysis,
  indent? : Int = 2,
) -> String {
  self.to_json().stringify(indent~)
}

///|
/// Look up a named rule's analysis. Rule names are compared exactly.
pub fn RuleSetAnalysis::find_rule(
  self : RuleSetAnalysis,
  name : String,
) -> RuleProgramAnalysis? {
  for rule in self.rules {
    if rule.name == name {
      return Some(rule)
    }
  }
  None
}

///|
/// Test whether any rule in the set references an exact normalized path.
pub fn RuleSetAnalysis::references_path(
  self : RuleSetAnalysis,
  path : String,
) -> Bool {
  self.referenced_paths.contains(path)
}

///|
/// Test whether any rule in the set calls the named function.
pub fn RuleSetAnalysis::uses_function(
  self : RuleSetAnalysis,
  name : String,
) -> Bool {
  self.called_functions.contains(name)
}

///|
/// Return true when at least one rule has a warning-level finding.
pub fn RuleSetAnalysis::has_warnings(self : RuleSetAnalysis) -> Bool {
  self.warning_count > 0
}

///|
fn append_unique_strings(
  target : Array[String],
  values : Array[String],
) -> Unit {
  for value in values {
    if !target.contains(value) {
      target.push(value)
    }
  }
}

///|
/// Analyze every compiled rule and combine its data dependencies and calls.
pub fn RuleSet::analyze(self : RuleSet) -> RuleSetAnalysis {
  let rules = []
  let called_functions = []
  let referenced_paths = []
  let mut total_nodes = 0
  let mut maximum_ast_depth = 0
  let mut estimated_cost = 0
  let mut warning_count = 0
  let mut note_count = 0
  for rule in self.rules {
    let analysis = analyze(rule.program)
    total_nodes = total_nodes + analysis.node_count
    estimated_cost = estimated_cost + analysis.estimated_cost
    warning_count = warning_count + analysis.warning_count()
    note_count = note_count + analysis.note_count()
    if analysis.ast_depth > maximum_ast_depth {
      maximum_ast_depth = analysis.ast_depth
    }
    append_unique_strings(called_functions, analysis.called_functions)
    append_unique_strings(referenced_paths, analysis.referenced_paths)
    rules.push({
      name: rule.definition.name,
      severity: rule.definition.severity,
      expression: rule.definition.expression,
      analysis,
    })
  }
  {
    rule_count: rules.length(),
    total_nodes,
    maximum_ast_depth,
    estimated_cost,
    warning_count,
    note_count,
    called_functions,
    referenced_paths,
    rules,
  }
}