///|
pub(all) struct ApiCompatibility {
  before_count : Int
  after_count : Int
  added : Array[String]
  removed : Array[String]
  required_bump : VersionBump
} derive(Debug, Eq)

///|
pub fn analyze_api_compatibility(
  before : String,
  after : String,
) -> ApiCompatibility {
  let before_declarations = api_public_declarations(before)
  let after_declarations = api_public_declarations(after)
  let added = api_difference(after_declarations, before_declarations)
  let removed = api_difference(before_declarations, after_declarations)
  let required_bump = if removed.length() > 0 {
    Major
  } else if added.length() > 0 {
    Minor
  } else {
    SameVersion
  }
  ApiCompatibility::{
    before_count: before_declarations.length(),
    after_count: after_declarations.length(),
    added,
    removed,
    required_bump,
  }
}

///|
pub fn ApiCompatibility::is_compatible(self : ApiCompatibility) -> Bool {
  self.removed.length() == 0
}

///|
pub fn ApiCompatibility::has_changes(self : ApiCompatibility) -> Bool {
  self.added.length() > 0 || self.removed.length() > 0
}

///|
pub fn ApiCompatibility::summary(self : ApiCompatibility) -> String {
  "api-compatible=" +
  bool_text(self.is_compatible()) +
  ", before=" +
  self.before_count.to_string() +
  ", after=" +
  self.after_count.to_string() +
  ", added=" +
  self.added.length().to_string() +
  ", removed=" +
  self.removed.length().to_string() +
  ", required-bump=" +
  version_bump_name(self.required_bump)
}

///|
pub fn ApiCompatibility::to_markdown(self : ApiCompatibility) -> String {
  let mut out = "## API Compatibility\n\n"
  out = out + "- Compatible: " + bool_text(self.is_compatible()) + "\n"
  out = out + "- Before declarations: " + self.before_count.to_string() + "\n"
  out = out + "- After declarations: " + self.after_count.to_string() + "\n"
  out = out +
    "- Required SemVer bump: " +
    version_bump_name(self.required_bump) +
    "\n"
  out = out + "- Added declarations: " + self.added.length().to_string() + "\n"
  out = out +
    "- Removed declarations: " +
    self.removed.length().to_string() +
    "\n"
  let with_added = api_changes_section(out, "Added", self.added)
  api_changes_section(with_added, "Removed", self.removed)
}

///|
pub fn api_snapshot_declarations(snapshot : String) -> Array[String] {
  api_public_declarations(snapshot)
}

///|
fn api_changes_section(
  current : String,
  title : String,
  changes : Array[String],
) -> String {
  if changes.length() == 0 {
    current
  } else {
    let mut out = current + "\n### " + title + "\n\n"
    let mut index = 0
    while index < changes.length() {
      out = out + "- `" + changes[index] + "`\n"
      index += 1
    }
    out
  }
}

///|
fn api_public_declarations(snapshot : String) -> Array[String] {
  let declarations : Array[String] = []
  let lines = split_lines(snapshot)
  let mut current = ""
  let mut in_block = false
  let mut index = 0
  while index < lines.length() {
    let line = trim_ascii(lines[index])
    if line.length() > 0 {
      if in_block {
        current = current + "\n" + line
        if contains(line, "}") {
          declarations.push(current)
          current = ""
          in_block = false
        }
      } else if starts_with(line, "pub ") || starts_with(line, "pub(") {
        if contains(line, "{") && !contains(line, "}") {
          current = line
          in_block = true
        } else {
          declarations.push(line)
        }
      }
    }
    index += 1
  }
  if current.length() > 0 {
    declarations.push(current)
  }
  declarations
}

///|
fn api_difference(left : Array[String], right : Array[String]) -> Array[String] {
  let difference : Array[String] = []
  let mut index = 0
  while index < left.length() {
    if !api_array_contains(right, left[index]) {
      difference.push(left[index])
    }
    index += 1
  }
  difference
}

///|
fn api_array_contains(items : Array[String], value : String) -> Bool {
  let mut index = 0
  while index < items.length() {
    if items[index] == value {
      return true
    }
    index += 1
  }
  false
}