///|
pub struct StringDelta {
  added : Array[String]
  removed : Array[String]
} derive(Eq, @debug.Debug)

///|
pub fn StringDelta::added(self : StringDelta) -> Array[String] {
  self.added.copy()
}

///|
pub fn StringDelta::removed(self : StringDelta) -> Array[String] {
  self.removed.copy()
}

///|
pub fn StringDelta::changed(self : StringDelta) -> Bool {
  !self.added.is_empty() || !self.removed.is_empty()
}

///|
fn string_delta(before : Array[String], after : Array[String]) -> StringDelta {
  let added : Array[String] = []
  let removed : Array[String] = []
  for value in after {
    if !contains_string(before, value) {
      added.push(value)
    }
  }
  for value in before {
    if !contains_string(after, value) {
      removed.push(value)
    }
  }
  added.sort_by(fn(left, right) { left.lexical_compare(right) })
  removed.sort_by(fn(left, right) { left.lexical_compare(right) })
  { added, removed, }
}

///|
fn kind_texts(values : Array[ChangeKind]) -> Array[String] {
  values.map(fn(kind) { kind.to_text() })
}

///|
pub struct PathPolicyDelta {
  path : String
  before : PathDecision
  after : PathDecision
  owners : StringDelta
  rules : StringDelta
  checks : StringDelta
  labels : StringDelta
  forbidden : StringDelta
} derive(Eq, @debug.Debug)

///|
pub fn PathPolicyDelta::path(self : PathPolicyDelta) -> String {
  self.path
}

///|
pub fn PathPolicyDelta::before(self : PathPolicyDelta) -> PathDecision {
  self.before
}

///|
pub fn PathPolicyDelta::after(self : PathPolicyDelta) -> PathDecision {
  self.after
}

///|
pub fn PathPolicyDelta::owners(self : PathPolicyDelta) -> StringDelta {
  self.owners
}

///|
pub fn PathPolicyDelta::rules(self : PathPolicyDelta) -> StringDelta {
  self.rules
}

///|
pub fn PathPolicyDelta::checks(self : PathPolicyDelta) -> StringDelta {
  self.checks
}

///|
pub fn PathPolicyDelta::labels(self : PathPolicyDelta) -> StringDelta {
  self.labels
}

///|
pub fn PathPolicyDelta::forbidden(self : PathPolicyDelta) -> StringDelta {
  self.forbidden
}

///|
pub fn PathPolicyDelta::changed(self : PathPolicyDelta) -> Bool {
  self.before != self.after
}

///|
pub struct PolicyComparison {
  paths : Array[PathPolicyDelta]
} derive(Eq, @debug.Debug)

///|
pub fn PolicyComparison::paths(
  self : PolicyComparison,
) -> Array[PathPolicyDelta] {
  self.paths.copy()
}

///|
pub fn PolicyComparison::changed_count(self : PolicyComparison) -> Int {
  let mut count = 0
  for path in self.paths {
    if path.changed() {
      count = count + 1
    }
  }
  count
}

///|
pub fn compare_policies(
  before : Policy,
  after : Policy,
  paths : Array[String],
) -> Result[PolicyComparison, Diagnostic] {
  if paths.is_empty() || paths.length() > 10000 {
    return Err(
      Diagnostic::new(
        "compare.paths.limit",
        "paths",
        "path list is empty or exceeds the limit",
        "1 through 10000 paths",
        paths.length().to_string(),
      ),
    )
  }
  let seen : Array[String] = []
  let deltas : Array[PathPolicyDelta] = []
  for index, path in paths {
    if contains_string(seen, path) {
      return Err(
        Diagnostic::new(
          "compare.path.duplicate",
          "paths[" + index.to_string() + "]",
          "path appears more than once",
          "unique path",
          path,
        ),
      )
    }
    let before_decision = match before.explain(path) {
      Ok(value) => value
      Err(_) =>
        return Err(
          Diagnostic::new(
            "compare.path.unsafe",
            "paths[" + index.to_string() + "]",
            "comparison path is unsafe",
            "safe repository-relative path",
            path,
          ),
        )
    }
    let after_decision = after.explain(path).unwrap()
    seen.push(path)
    deltas.push({
      path,
      before: before_decision,
      after: after_decision,
      owners: string_delta(before_decision.owners, after_decision.owners),
      rules: string_delta(
        before_decision.matched_rules,
        after_decision.matched_rules,
      ),
      checks: string_delta(before_decision.checks, after_decision.checks),
      labels: string_delta(before_decision.labels, after_decision.labels),
      forbidden: string_delta(
        kind_texts(before_decision.forbidden),
        kind_texts(after_decision.forbidden),
      ),
    })
  }
  Ok({ paths: deltas, })
}

///|
fn delta_text(name : String, delta : StringDelta) -> String {
  name +
  "+=" +
  format_optional_csv(delta.added) +
  " " +
  name +
  "-=" +
  format_optional_csv(delta.removed)
}

///|
pub fn PolicyComparison::to_text(self : PolicyComparison) -> String {
  let output = StringBuilder()
  output.write_string(
    "MOONCHANGE_COMPARE 1\nPATHS " +
    self.paths.length().to_string() +
    "\nCHANGED " +
    self.changed_count().to_string(),
  )
  for delta in self.paths {
    let state = if delta.changed() { "changed" } else { "unchanged" }
    output.write_string(
      "\nPATH " +
      delta.path +
      " " +
      state +
      "\n  " +
      delta_text("owners", delta.owners) +
      "\n  approvals=" +
      delta.before.approvals.to_string() +
      "=>" +
      delta.after.approvals.to_string() +
      "\n  " +
      delta_text("checks", delta.checks) +
      "\n  " +
      delta_text("labels", delta.labels) +
      "\n  " +
      delta_text("forbid", delta.forbidden) +
      "\n  max_lines=" +
      delta.before.max_lines.map(fn(value) { value.to_string() }).unwrap_or("-") +
      "=>" +
      delta.after.max_lines.map(fn(value) { value.to_string() }).unwrap_or("-") +
      "\n  release_note=" +
      (if delta.before.release_note { "yes" } else { "no" }) +
      "=>" +
      (if delta.after.release_note { "yes" } else { "no" }) +
      "\n  binary=" +
      (if delta.before.allow_binary { "allow" } else { "deny" }) +
      "=>" +
      (if delta.after.allow_binary { "allow" } else { "deny" }),
    )
  }
  output.to_string()
}