///|
pub(all) enum ChangeKind {
  Added
  Removed
  Modified
} derive(Eq, Debug)

///|
pub(all) struct Change {
  kind : ChangeKind
  path : String
  before : String?
  after : String?
  preview_truncated : Bool
} derive(Eq, Debug)

///|
pub(all) struct Comparison {
  total_changes : Int32
  changes : Array[Change]
  truncated : Bool
} derive(Eq, Debug)

///|
fn path_escape(text : String) -> String {
  text.replace(old="~", new="~0").replace(old="/", new="~1")
}

///|
fn canonical_number_or_word(text : String) -> String {
  decimal_identity(text).unwrap_or(text)
}

///|
fn canonical_atom(atom : Atom) -> String {
  match atom {
    Word(text) => canonical_number_or_word(text)
    Text(text) => quoted_text(text)
  }
}

///|
fn canonical_value(name : String, atoms : Array[Atom]) -> String {
  let text = atoms_text(atoms)
  if name == "function" ||
    name == "when" ||
    name == "three_state" ||
    name == "next_state" ||
    name == "clocked_on" ||
    name == "enable" {
    try parse_logic(text) catch {
      _ => ()
    } noraise {
      expression => {
        let formatted = format_logic(expression) catch { _ => return text }
        return formatted
      }
    }
  }
  if name == "related_pin" {
    // Order can map corresponding members of buses/bundles. Never sort it away.
    return words(text).join(" ")
  }
  atoms.map(canonical_atom).join(" ")
}

///|
fn canonical_argument(name : String, atoms : Array[Atom]) -> String {
  if ["index_1", "index_2", "index_3", "values"].contains(name) {
    if atoms is [atom] {
      let values = []
      let mut numeric = true
      for part in atom_text(atom).split(",") {
        match decimal_identity(part.trim().to_owned()) {
          Some(value) => values.push(value)
          None => {
            numeric = false
            break
          }
        }
      }
      if numeric {
        return values.join(",")
      }
    }
  }
  atoms.map(canonical_atom).join(" ")
}

///|
fn group_identity(group : Group) -> String {
  if !group.name().is_empty() {
    return group.kind + ":" + group.name()
  }
  if group.kind == "timing" || group.kind == "internal_power" {
    let related = match group.text("related_pin") {
      Some(value) => canonical_value("related_pin", [Text(value)])
      None => ""
    }
    let condition = match group.text("when") {
      Some(value) => canonical_value("when", [Text(value)])
      None => ""
    }
    let timing_type = match group.text("timing_type") {
      Some(value) => value
      None => ""
    }
    return group.kind +
      ":" +
      [related, condition, timing_type].map(quoted_text).join(",")
  }
  group.kind +
  ":" +
  group.arguments.map(args => args.map(canonical_atom).join(" ")).join(",")
}

///|
fn put_entry(
  entries : Map[String, String],
  key : String,
  value : String,
  pos : Position,
) -> Unit raise LibertyError {
  if entries.length() >= 200000 {
    raise failure(
      "DiffLimit", "semantic comparison exceeds 200000 entries", pos,
    )
  }
  entries[key] = value
}

///|
fn flatten_group(
  group : Group,
  path : String,
  entries : Map[String, String],
  depth : Int32,
) -> Unit raise LibertyError {
  if depth > 128 {
    raise failure("DiffLimit", "comparison depth exceeds 128", group.position)
  }
  // An explicit group marker keeps empty added/removed cells visible.
  put_entry(entries, path + "/@group", group.kind, group.position)
  let occurrences : Map[String, Int32] = Map([])
  for item in group.items {
    let (key, value) = match item {
      Attribute(name, atoms, _) =>
        ("@" + name, Some(canonical_value(name, atoms)))
      Complex(name, arguments, _) =>
        (
          "call:" + name,
          Some(
            arguments
            .map(args => canonical_argument(name, args))
            .map(quoted_text)
            .join(","),
          ),
        )
      Section(child) => (group_identity(child), None)
    }
    let number = match occurrences.get(key) {
      Some(number) => number
      None => 0
    }
    occurrences[key] = number + 1
    let child_path = path + "/" + path_escape(key) + "#" + number.to_string()
    match item {
      Section(child) => flatten_group(child, child_path, entries, depth + 1)
      _ => put_entry(entries, child_path, value.unwrap(), group.position)
    }
  }
}

///|
fn semantic_entries(
  library : Library,
) -> Map[String, String] raise LibertyError {
  let entries : Map[String, String] = Map([])
  entries["library/@name"] = library.name
  flatten_group(library.source, "library", entries, 0)
  entries
}

///|
fn preview(value : String) -> String {
  if value.length() > 1024 {
    value[0:1024].to_owned() + "..."
  } else {
    value
  }
}

///|
pub fn compare_libraries(
  left : Library,
  right : Library,
  max_changes~ : Int32 = 10000,
) -> Comparison raise LibertyError {
  if max_changes < 1 || max_changes > 100000 {
    raise failure(
      "InvalidLimit",
      "max_changes must be in 1..100000",
      no_position(),
    )
  }
  for library in [left, right] {
    let errors = lint_library(library).filter(f => f.severity == Error)
    if !errors.is_empty() {
      raise failure(
        "InvalidLibrary",
        errors[0].code + ": " + errors[0].message,
        errors[0].position,
      )
    }
  }
  let a = semantic_entries(left)
  let b = semantic_entries(right)
  let all_keys : Map[String, Bool] = Map([])
  for key, _ in a {
    all_keys[key] = true
  }
  for key, _ in b {
    all_keys[key] = true
  }
  let keys = all_keys.keys().to_array()
  keys.sort_by(lexical_compare)
  let changes = []
  let mut total = 0
  for path in keys {
    let before = a.get(path)
    let after = b.get(path)
    if before == after {
      continue
    }
    total += 1
    if changes.length() >= max_changes {
      continue
    }
    let kind = match (before, after) {
      (None, Some(_)) => Added
      (Some(_), None) => Removed
      _ => Modified
    }
    changes.push({
      kind,
      path,
      before: before.map(preview),
      after: after.map(preview),
      preview_truncated: (before is Some(value) && value.length() > 1024) ||
      (after is Some(value) && value.length() > 1024),
    })
  }
  { total_changes: total, changes, truncated: total > changes.length(), }
}

///|
pub fn comparison_json(comparison : Comparison) -> Json {
  {
    "schema": "moonliberty.diff.v1",
    "has_changes": Json(comparison.total_changes != 0),
    "total_changes": Json(comparison.total_changes),
    "truncated": Json(comparison.truncated),
    "scope": "group/attribute semantics, numeric and Boolean syntax normalization; not electrical equivalence or STA signoff",
    "changes": Json(
      comparison.changes.map(change => {
        (
          {
            "kind": Json(
              match change.kind {
                Added => "added"
                Removed => "removed"
                Modified => "modified"
              },
            ),
            "path": Json(change.path),
            "before": nullable_text(change.before),
            "after": nullable_text(change.after),
            "preview_truncated": Json(change.preview_truncated),
          } : Json)
      }),
    ),
  }
}

///|
pub fn select_cells(
  library : Library,
  names : Array[String],
) -> Group raise LibertyError {
  if names.is_empty() {
    raise failure(
      "EmptySelection",
      "select at least one cell",
      library.source.position,
    )
  }
  for name in names {
    if library.cell(name) is None {
      raise failure(
        "MissingCell",
        "cannot extract unknown cell " + name,
        library.source.position,
      )
    }
  }
  {
    ..library.source,
    items: library.source.items.filter(item => {
      match item {
        Section(group) if group.kind == "cell" => names.contains(group.name())
        _ => true
      }
    }),
  }
}