///|
/// Structural changes between two compiled templates. This is a source-level
/// report for API reviews, not a proof that two templates render equivalent
/// URIs or that a deployment is backward compatible.
pub struct TemplateDiff {
  added_variables : Array[String]
  removed_variables : Array[String]
  became_scalar_only : Array[String]
  new_repeated_variables : Array[String]
  old_level : Int
  new_level : Int
  reserved_added : Bool
  fragment_added : Bool
}

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

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

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

///|
pub fn TemplateDiff::new_repeated_variables(
  self : TemplateDiff,
) -> Array[String] {
  self.new_repeated_variables.copy()
}

///|
pub fn TemplateDiff::old_level(self : TemplateDiff) -> Int {
  self.old_level
}

///|
pub fn TemplateDiff::new_level(self : TemplateDiff) -> Int {
  self.new_level
}

///|
pub fn TemplateDiff::reserved_added(self : TemplateDiff) -> Bool {
  self.reserved_added
}

///|
pub fn TemplateDiff::fragment_added(self : TemplateDiff) -> Bool {
  self.fragment_added
}

///|
pub fn TemplateDiff::has_flagged_changes(self : TemplateDiff) -> Bool {
  !self.added_variables.is_empty() ||
  !self.removed_variables.is_empty() ||
  !self.became_scalar_only.is_empty() ||
  !self.new_repeated_variables.is_empty() ||
  self.old_level != self.new_level ||
  self.reserved_added ||
  self.fragment_added
}

///|
fn Template::uses_fragment(self : Template) -> Bool {
  for expression in self.analyze().expressions() {
    if expression.operator == "#" {
      return true
    }
  }
  false
}

///|
/// Compare variable and operator requirements before replacing a deployed
/// template. Added variables matter to `expand_required`; newly scalar-only
/// variables can reject previously valid composite bindings.
pub fn Template::diff_contract(
  self : Template,
  next : Template,
) -> TemplateDiff {
  let old_profiles = self.variable_profiles()
  let new_profiles = next.variable_profiles()
  let old_by_name : Map[String, VariableProfile] = Map([])
  let new_by_name : Map[String, VariableProfile] = Map([])
  for profile in old_profiles {
    old_by_name[profile.name] = profile
  }
  for profile in new_profiles {
    new_by_name[profile.name] = profile
  }
  let added_variables : Array[String] = []
  let removed_variables : Array[String] = []
  let became_scalar_only : Array[String] = []
  let new_repeated_variables : Array[String] = []
  for profile in new_profiles {
    match old_by_name.get(profile.name) {
      None => added_variables.push(profile.name)
      Some(previous) => {
        if profile.scalar_only && !previous.scalar_only {
          became_scalar_only.push(profile.name)
        }
        if profile.occurrences > 1 && profile.occurrences > previous.occurrences {
          new_repeated_variables.push(profile.name)
        }
      }
    }
  }
  for profile in old_profiles {
    if !new_by_name.contains(profile.name) {
      removed_variables.push(profile.name)
    }
  }
  let before = self.analyze()
  let after = next.analyze()
  {
    added_variables,
    removed_variables,
    became_scalar_only,
    new_repeated_variables,
    old_level: before.max_level(),
    new_level: after.max_level(),
    reserved_added: after.has_reserved_expansion() &&
    !before.has_reserved_expansion(),
    fragment_added: next.uses_fragment() && !self.uses_fragment(),
  }
}