///|
/// A semantic change between two canonical PSL snapshots.
pub(all) enum SnapshotChange {
  AddedRule(String, RuleSection)
  RemovedRule(String, RuleSection)
  MovedRule(String, RuleSection, RuleSection)
} derive(Eq, Debug)

///|
/// A deterministic semantic comparison between two snapshots.
pub struct SnapshotDiff {
  from_revision_ : String
  to_revision_ : String
  from_sha256_ : String
  to_sha256_ : String
  changes_ : ReadOnlyArray[SnapshotChange]
  added_count_ : Int
  removed_count_ : Int
  moved_count_ : Int
}

///|
fn section_bit(section : RuleSection) -> UInt {
  match section {
    UnsectionedRule => 1U
    IcannSection => 2U
    PrivateSection => 4U
  }
}

///|
fn add_rules_to_mask(
  masks : Map[String, UInt],
  rules : Array[String],
  section : RuleSection,
) -> Unit {
  let bit = section_bit(section)
  for rule in rules {
    let previous = masks.get(rule).unwrap_or(0U)
    masks.set(rule, previous | bit)
  }
}

///|
fn SuffixList::rule_section_masks(self : SuffixList) -> Map[String, UInt] {
  let masks : Map[String, UInt] = Map([])
  let (unsectioned, icann, private_rules) = self.sorted_rules_by_section()
  add_rules_to_mask(masks, unsectioned, UnsectionedRule)
  add_rules_to_mask(masks, icann, IcannSection)
  add_rules_to_mask(masks, private_rules, PrivateSection)
  masks
}

///|
fn sections_in_mask(mask : UInt) -> Array[RuleSection] {
  let sections : Array[RuleSection] = []
  for section in [UnsectionedRule, IcannSection, PrivateSection] {
    if (mask & section_bit(section)) != 0U {
      sections.push(section)
    }
  }
  sections
}

///|
/// Compare this snapshot with a newer snapshot.
///
/// A rule is classified as moved only when exactly one old section disappears
/// and exactly one new section appears. Ambiguous multi-section changes remain
/// explicit additions and removals.
pub fn Snapshot::diff(self : Snapshot, newer : Snapshot) -> SnapshotDiff {
  let before = SuffixList::parse(self.psl_text_).unwrap()
  let after = SuffixList::parse(newer.psl_text_).unwrap()
  let before_masks = before.rule_section_masks()
  let after_masks = after.rule_section_masks()
  let all_rules : Map[String, Bool] = Map([])
  for rule, _ in before_masks {
    all_rules.set(rule, true)
  }
  for rule, _ in after_masks {
    all_rules.set(rule, true)
  }
  let rules : Array[String] = []
  for rule, _ in all_rules {
    rules.push(rule)
  }
  rules.sort_by(compare_code_units)
  let changes : Array[SnapshotChange] = []
  let mut added_count = 0
  let mut removed_count = 0
  let mut moved_count = 0
  for rule in rules {
    let before_mask = before_masks.get(rule).unwrap_or(0U)
    let after_mask = after_masks.get(rule).unwrap_or(0U)
    let removed = sections_in_mask(before_mask & after_mask.lnot())
    let added = sections_in_mask(after_mask & before_mask.lnot())
    if removed.length() == 1 && added.length() == 1 {
      changes.push(MovedRule(rule, removed[0], added[0]))
      moved_count = moved_count + 1
    } else {
      for section in removed {
        changes.push(RemovedRule(rule, section))
        removed_count = removed_count + 1
      }
      for section in added {
        changes.push(AddedRule(rule, section))
        added_count = added_count + 1
      }
    }
  }
  {
    from_revision_: self.source_revision_,
    to_revision_: newer.source_revision_,
    from_sha256_: self.sha256_,
    to_sha256_: newer.sha256_,
    changes_: ReadOnlyArray::from_array(changes),
    added_count_: added_count,
    removed_count_: removed_count,
    moved_count_: moved_count,
  }
}

///|
pub fn SnapshotDiff::from_revision(self : SnapshotDiff) -> String {
  self.from_revision_
}

///|
pub fn SnapshotDiff::to_revision(self : SnapshotDiff) -> String {
  self.to_revision_
}

///|
pub fn SnapshotDiff::from_sha256(self : SnapshotDiff) -> String {
  self.from_sha256_
}

///|
pub fn SnapshotDiff::to_sha256(self : SnapshotDiff) -> String {
  self.to_sha256_
}

///|
pub fn SnapshotDiff::changes(
  self : SnapshotDiff,
) -> ReadOnlyArray[SnapshotChange] {
  self.changes_
}

///|
pub fn SnapshotDiff::added_count(self : SnapshotDiff) -> Int {
  self.added_count_
}

///|
pub fn SnapshotDiff::removed_count(self : SnapshotDiff) -> Int {
  self.removed_count_
}

///|
pub fn SnapshotDiff::moved_count(self : SnapshotDiff) -> Int {
  self.moved_count_
}

///|
pub fn SnapshotDiff::is_empty(self : SnapshotDiff) -> Bool {
  self.changes_.length() == 0
}

///|
fn write_change_line(output : StringBuilder, change : SnapshotChange) -> Unit {
  match change {
    AddedRule(rule, section) =>
      output.write_string("+ [\{section_name(section)}] \{rule}\n")
    RemovedRule(rule, section) =>
      output.write_string("- [\{section_name(section)}] \{rule}\n")
    MovedRule(rule, from, to) =>
      output.write_string(
        "> [\{section_name(from)}->\{section_name(to)}] \{rule}\n",
      )
  }
}

///|
/// Return a deterministic, line-oriented semantic change report.
pub fn SnapshotDiff::report_text(self : SnapshotDiff) -> String {
  let output = StringBuilder()
  output.write_string("format=moonsuffix-diff-v1\n")
  output.write_string("from_revision=\{manifest_escape(self.from_revision_)}\n")
  output.write_string("to_revision=\{manifest_escape(self.to_revision_)}\n")
  output.write_string("from_sha256=\{self.from_sha256_}\n")
  output.write_string("to_sha256=\{self.to_sha256_}\n")
  output.write_string("added=\{self.added_count_}\n")
  output.write_string("removed=\{self.removed_count_}\n")
  output.write_string("moved=\{self.moved_count_}\n")
  output.write_string("changes:\n")
  for change in self.changes_ {
    write_change_line(output, change)
  }
  output.to_string()
}