///|
priv enum RuleDeclarationRole {
  InputOnly
  OutputOnly
  InputAndOutput
}

///|
priv struct LiveRuleDeclaration {
  rule_id : CellId
  role : RuleDeclarationRole
}

///|
fn find_live_rule_declaration(
  core : RuntimeCore,
  rules : Array[RuleData],
  relation_id : CellId,
) -> LiveRuleDeclaration? {
  for rule in rules {
    guard !@kernel.is_cell_disposed(core, rule.meta.cell_id) else { continue }
    let is_input = rule.input_relations.contains(relation_id)
    let is_output = rule.output_relations.contains(relation_id)
    if is_input || is_output {
      let role = if is_input && is_output {
        InputAndOutput
      } else if is_input {
        InputOnly
      } else {
        OutputOnly
      }
      return Some({ rule_id: rule.meta.cell_id, role })
    }
  }
  None
}

///|
/// Registers a Datalog rule over declared input and output relations.
///
/// The declaration arrays are snapshotted. A live rule pins every relation it
/// declares, so dispose each declaring rule before disposing those relations.
/// Panics if a declared cell belongs to another runtime, is disposed, or is
/// not a relation.
pub fn Runtime::new_rule(
  self : Runtime,
  input_relations : Array[CellId],
  output_relations : Array[CellId],
  apply_delta : () -> Unit,
  label? : String,
) -> @incr_types.RuleId {
  for id in input_relations {
    self.assert_rule_relation_id(id, "input")
  }
  for id in output_relations {
    self.assert_rule_relation_id(id, "output")
  }
  let idx = self.datalog.rules.length()
  let cell_id = self.alloc_cell_id(Rule(idx))
  let data : RuleData = {
    meta: {
      cell_id,
      label,
      changed_at: Revision::initial(),
      durability: Low,
      subscribers: @hashset.HashSet([]),
      push_reachable_count: 0,
    },
    apply_delta,
    input_relations: input_relations.copy(),
    output_relations: output_relations.copy(),
  }
  self.datalog.rules.push(data)
  let ops : &CellOps = self.datalog.rules[idx]
  self.core.cell_ops.push(ops)
  let lifecycle : &CellLifecycle = self.datalog.rules[idx]
  self.cell_lifecycle.push(lifecycle)
  @incr_types.RuleId::{ id: cell_id }
}

///|
fn Runtime::assert_rule_relation_id(
  self : Runtime,
  id : CellId,
  role : String,
) -> Unit {
  if id.runtime_id != self.core.runtime_id {
    abort(
      "new_rule: " +
      role +
      " relation belongs to Runtime " +
      id.runtime_id.to_string() +
      ", expected Runtime " +
      self.core.runtime_id.to_string(),
    )
  }
  if id.id < 0 || id.id >= self.core.cell_index.length() {
    abort(
      "new_rule: " +
      role +
      " relation cell_id out of bounds: " +
      id.id.to_string(),
    )
  }
  match self.core.cell_index[id.id] {
    Relation(_) | FunctionalRelation(_) => ()
    Disposed =>
      abort(
        "new_rule: " +
        role +
        " relation cell_id is disposed: " +
        id.id.to_string(),
      )
    _ =>
      abort(
        "new_rule: " + role + " cell_id is not a Relation: " + id.id.to_string(),
      )
  }
}