///| ABI compatibility engine.
///
/// The engine answers one question: can callers that used the old contract keep
/// using the new one through the same interface? It compares canonical
/// signatures and selectors, so renaming a parameter, reordering JSON keys, or
/// recompiling changes nothing, while changing a parameter type does, because
/// the selector changes.
///
/// Severity follows the caller, not the source:
///
/// - `Error`, because a caller breaks: a signature that disappeared from any
///   category; a function whose output types changed, because callers decode
///   return data positionally; an event whose indexed parameters changed,
///   because topic filters no longer select the same logs; an event that became
///   or stopped being anonymous, because its topic disappears or appears; and a
///   selector or topic that now belongs to a different signature. Losing
///   payability or the static-call guarantee is also an error.
/// - `Warning`, because the interface still resolves but its contract changed:
///   a non-breaking function state-mutability change. The selector and calldata
///   are unchanged, so this remains a behavioural change for review.
/// - `Info`, because the change is additive: a new function, event, or error.
///
/// Constructors are not compared: their inputs affect deployment, not the
/// interface an existing proxy exposes. A fallback or receive entry is compared
/// like a function without parameters, so removing one is an error, but it
/// matches within its own kind: a fallback handler and a named function
/// `fallback()` share a signature, and neither can stand in for the other,
/// because one answers otherwise-unmatched calldata and the other is
/// selector-dispatched.
///
/// One invariant comes from the compiler rather than from this engine: a
/// selector is unique within one ABI, because the compiler rejects two
/// functions that would share one. The engine therefore checks discriminators
/// across the two versions, which is where a collision can actually appear,
/// and not within one ABI.

///|
/// Compares two normalized ABIs.
///
/// Returns the diagnostics that describe the change, sorted. An empty result
/// means every old signature, output list, indexed layout, and discriminator
/// survived unchanged.
pub fn compare_abi(old : Abi, new : Abi) -> Array[Diagnostic] {
  let diagnostics : Array[Diagnostic] = []
  compare_functions(old.functions, new.functions, diagnostics)
  compare_events(old.events, new.events, diagnostics)
  compare_errors(old.errors, new.errors, diagnostics)
  compare_collisions(old, new, diagnostics)
  diagnostics.sort()
  diagnostics
}

///|
/// Splits two entry lists into the entries only in the old version, only in the
/// new version, and the pairs that share a canonical signature.
///
/// Duplicate signatures are paired in input order, which is deterministic
/// because the ABI array order comes from the artifact.
fn[T] match_signatures(
  old : Array[T],
  new : Array[T],
  signature : (T) -> String,
) -> (Array[T], Array[T], Array[(T, T)]) {
  let matched_new : Array[Bool] = Array::make(new.length(), false)
  let removed : Array[T] = []
  let matched : Array[(T, T)] = []
  for old_entry in old {
    let mut found = -1
    for j in 0..= 0 {
      matched_new[found] = true
      matched.push((old_entry, new[found]))
    } else {
      removed.push(old_entry)
    }
  }
  let added : Array[T] = []
  for j in 0.. Location {
  { contract: None, path: "abi." + section, }
}

///|
/// Location of one ABI entry, named by its canonical signature.
fn abi_entry_location(section : String, signature : String) -> Location {
  { contract: None, path: "abi." + section + "[\"" + signature + "\"]", }
}

///|
/// Compares callable entries, including fallback and receive.
fn compare_functions(
  old : Array[AbiFunction],
  new : Array[AbiFunction],
  diagnostics : Array[Diagnostic],
) -> Unit {
  // Matching pairs an old entry with a new one only when both the kind and the
  // canonical signature agree. The signature alone is not enough: a fallback
  // handler and a named function `fallback()` hash to the same string, but a
  // caller reaches them through different dispatch.
  let (removed, added, matched) = match_signatures(old, new, fn(entry) {
    entry_noun(entry.kind) + ":" + entry.signature
  })
  for entry in removed {
    diagnostics.push(
      Diagnostic::new(
        AbiFunctionRemoved,
        abi_section_location("functions"),
        "\{entry_noun(entry.kind)} \"\{entry.signature}\" is missing from the new ABI",
      ).with_values(old_value=entry.signature),
    )
  }
  for entry in added {
    diagnostics.push(
      Diagnostic::new(
        AbiFunctionAdded,
        abi_section_location("functions"),
        "\{entry_noun(entry.kind)} \"\{entry.signature}\" was added to the new ABI",
      ).with_values(new_value=entry.signature),
    )
  }
  for pair in matched {
    let (old_entry, new_entry) = pair
    let noun = entry_noun(old_entry.kind)
    let entry_path = abi_entry_location("functions", old_entry.signature).path
    if old_entry.mutability != new_entry.mutability {
      let code = if incompatible_mutability(
          old_entry.mutability,
          new_entry.mutability,
        ) {
        AbiFunctionMutabilityIncompatible
      } else {
        AbiFunctionMutabilityChanged
      }
      diagnostics.push(
        Diagnostic::new(
          code,
          { contract: None, path: entry_path + ".stateMutability", },
          "\{noun} \"\{old_entry.signature}\" changed state mutability from \"\{old_entry.mutability.name()}\" to \"\{new_entry.mutability.name()}\"",
        ).with_values(
          old_value=old_entry.mutability.name(),
          new_value=new_entry.mutability.name(),
        ),
      )
    }
    let old_outputs = output_types(old_entry)
    let new_outputs = output_types(new_entry)
    if old_outputs != new_outputs {
      diagnostics.push(
        Diagnostic::new(
          AbiFunctionOutputsChanged,
          { contract: None, path: entry_path + ".outputs", },
          "\{noun} \"\{old_entry.signature}\" changed its output types from \"\{old_outputs}\" to \"\{new_outputs}\"",
        ).with_values(old_value=old_outputs, new_value=new_outputs),
      )
    }
  }
}

///|
/// Whether a mutability change removes behavior existing callers may rely on.
/// Payable callers may send value, while callers compiled against a pure or
/// view interface use a static call that cannot execute newly permitted writes.
fn incompatible_mutability(old : Mutability, new : Mutability) -> Bool {
  match old {
    Payable => new != Payable
    Pure | View => new == NonPayable || new == Payable
    NonPayable => false
  }
}

///|
/// Compares events.
fn compare_events(
  old : Array[AbiEvent],
  new : Array[AbiEvent],
  diagnostics : Array[Diagnostic],
) -> Unit {
  let (removed, added, matched) = match_signatures(old, new, fn(entry) {
    entry.signature
  })
  for entry in removed {
    diagnostics.push(
      Diagnostic::new(
        AbiEventRemoved,
        abi_section_location("events"),
        "event \"\{entry.signature}\" is missing from the new ABI",
      ).with_values(old_value=entry.signature),
    )
  }
  for entry in added {
    diagnostics.push(
      Diagnostic::new(
        AbiEventAdded,
        abi_section_location("events"),
        "event \"\{entry.signature}\" was added to the new ABI",
      ).with_values(new_value=entry.signature),
    )
  }
  for pair in matched {
    let (old_entry, new_entry) = pair
    let entry_path = abi_entry_location("events", old_entry.signature).path
    let old_indexed = indexed_flags(old_entry)
    let new_indexed = indexed_flags(new_entry)
    if old_indexed != new_indexed {
      diagnostics.push(
        Diagnostic::new(
          AbiEventIndexedChanged,
          { contract: None, path: entry_path + ".inputs", },
          "event \"\{old_entry.signature}\" changed its indexed parameters from \"\{old_indexed}\" to \"\{new_indexed}\"",
        ).with_values(old_value=old_indexed, new_value=new_indexed),
      )
    }
    if old_entry.anonymous != new_entry.anonymous {
      diagnostics.push(
        Diagnostic::new(
          AbiEventAnonymousChanged,
          { contract: None, path: entry_path + ".anonymous", },
          "event \"\{old_entry.signature}\" changed from anonymous=\{old_entry.anonymous} to anonymous=\{new_entry.anonymous}",
        ).with_values(
          old_value=old_entry.anonymous.to_string(),
          new_value=new_entry.anonymous.to_string(),
        ),
      )
    }
  }
}

///|
/// Compares custom errors. Matching signatures have identical inputs by
/// construction, so only appearance and disappearance are reported.
fn compare_errors(
  old : Array[AbiError],
  new : Array[AbiError],
  diagnostics : Array[Diagnostic],
) -> Unit {
  let (removed, added, _) = match_signatures(old, new, fn(entry) {
    entry.signature
  })
  for entry in removed {
    diagnostics.push(
      Diagnostic::new(
        AbiErrorRemoved,
        abi_section_location("errors"),
        "error \"\{entry.signature}\" is missing from the new ABI",
      ).with_values(old_value=entry.signature),
    )
  }
  for entry in added {
    diagnostics.push(
      Diagnostic::new(
        AbiErrorAdded,
        abi_section_location("errors"),
        "error \"\{entry.signature}\" was added to the new ABI",
      ).with_values(new_value=entry.signature),
    )
  }
}

///|
/// Reports discriminators that the two versions use for different signatures.
///
/// A function selector is four bytes of call data and an event topic is the
/// hash of the event signature. When the new version hands one of them to a
/// different signature, a caller that keeps the old calldata or the old filter
/// silently reaches something else, which no per-signature comparison can see.
fn compare_collisions(
  old : Abi,
  new : Abi,
  diagnostics : Array[Diagnostic],
) -> Unit {
  // Only real functions are dispatched by selector: a fallback or receive
  // entry answers any selector that no function claims, so it cannot take one
  // over.
  let old_functions : Array[AbiFunction] = []
  for entry in old.functions {
    if entry.kind == Function {
      old_functions.push(entry)
    }
  }
  let new_functions : Array[AbiFunction] = []
  for entry in new.functions {
    if entry.kind == Function {
      new_functions.push(entry)
    }
  }
  report_collisions(
    old_functions,
    new_functions,
    fn(entry) { entry.signature },
    fn(entry) { entry.selector },
    "functions",
    "function selector",
    diagnostics,
  )
  report_collisions(
    old.errors,
    new.errors,
    fn(entry) { entry.signature },
    fn(entry) { entry.selector },
    "errors",
    "error selector",
    diagnostics,
  )
  // An anonymous event occupies no topic, so it cannot take one over.
  let old_events : Array[AbiEvent] = []
  for entry in old.events {
    if !entry.anonymous {
      old_events.push(entry)
    }
  }
  let new_events : Array[AbiEvent] = []
  for entry in new.events {
    if !entry.anonymous {
      new_events.push(entry)
    }
  }
  report_collisions(
    old_events,
    new_events,
    fn(entry) { entry.signature },
    fn(entry) { entry.topic },
    "events",
    "event topic",
    diagnostics,
  )
}

///|
/// Reports every pair of entries that shares a discriminator with a different
/// signature.
fn[T] report_collisions(
  old : Array[T],
  new : Array[T],
  signature : (T) -> String,
  discriminator : (T) -> String,
  section : String,
  noun : String,
  diagnostics : Array[Diagnostic],
) -> Unit {
  for old_entry in old {
    for new_entry in new {
      if discriminator(old_entry) == discriminator(new_entry) &&
        signature(old_entry) != signature(new_entry) {
        let discriminator_value = discriminator(old_entry)
        diagnostics.push(
          Diagnostic::new(
            AbiSignatureCollision,
            abi_section_location(section),
            "\{noun} \{discriminator_value} is used by \"\{signature(old_entry)}\" in the old ABI and \"\{signature(new_entry)}\" in the new ABI",
          ).with_values(
            old_value=signature(old_entry),
            new_value=signature(new_entry),
          ),
        )
      }
    }
  }
}

///|
/// The word used for a callable entry in messages.
fn entry_noun(kind : AbiEntryKind) -> String {
  match kind {
    Function => "function"
    Fallback => "fallback"
    Receive => "receive"
  }
}

///|
/// Canonical output type list, which is what a caller decodes.
fn output_types(entry : AbiFunction) -> String {
  let types : Array[String] = entry.outputs.map(fn(param) { param.type_name })
  types.join(",")
}

///|
/// Which parameters of an event are indexed, as one readable value.
fn indexed_flags(entry : AbiEvent) -> String {
  let flags : Array[String] = entry.inputs.map(fn(param) {
    if param.indexed {
      "indexed"
    } else {
      "not indexed"
    }
  })
  flags.join(", ")
}