///|
fn entity_position(entities : Array[Entity], id : Int) -> Int? {
  for index, entity in entities {
    if entity.id == id {
      return Some(index)
    }
  }
  None
}

///|
fn entity_patch_id(operation : EntityPatch) -> Int {
  match operation {
    Add(entity) => entity.id
    Remove(id) => id
    Replace(entity) => entity.id
    SetAttribute(id~, ..) => id
  }
}

///|
fn entity_patch_name(operation : EntityPatch) -> String {
  match operation {
    Add(entity) => "add " + entity.name
    Remove(id) => "remove #" + id.to_string()
    Replace(entity) => "replace #" + entity.id.to_string() + " " + entity.name
    SetAttribute(id~, index~, ..) =>
      "set #" + id.to_string() + " attribute " + index.to_string()
  }
}

///|
pub fn EntityPatch::id(self : EntityPatch) -> Int {
  entity_patch_id(self)
}

///|
pub fn EntityPatch::description(self : EntityPatch) -> String {
  entity_patch_name(self)
}

///|
pub fn ModelPatch::new() -> ModelPatch {
  { operations: [], added: 0, removed: 0, replaced: 0, changed_attributes: 0 }
}

///|
pub fn ModelPatch::operation_count(self : ModelPatch) -> Int {
  self.operations.length()
}

///|
pub fn ModelPatch::is_empty(self : ModelPatch) -> Bool {
  self.operations.length() == 0
}

///|
pub fn ModelPatch::push(self : ModelPatch, operation : EntityPatch) -> Unit {
  self.operations.push(operation)
  match operation {
    Add(_) => self.added += 1
    Remove(_) => self.removed += 1
    Replace(_) => self.replaced += 1
    SetAttribute(..) => self.changed_attributes += 1
  }
}

///|
pub fn ModelPatch::add(self : ModelPatch, entity : Entity) -> Unit {
  self.push(Add(entity))
}

///|
pub fn ModelPatch::remove(self : ModelPatch, id : Int) -> Unit {
  self.push(Remove(id))
}

///|
pub fn ModelPatch::replace(self : ModelPatch, entity : Entity) -> Unit {
  self.push(Replace(entity))
}

///|
pub fn ModelPatch::set_attribute(
  self : ModelPatch,
  id : Int,
  index : Int,
  value : Value,
) -> Unit {
  self.push(SetAttribute(id~, index~, value~))
}

///|
fn add_entity(
  entities : Array[Entity],
  entity : Entity,
) -> Unit raise EditError {
  if entity_position(entities, entity.id) is Some(_) {
    raise DuplicateEntity(id=entity.id)
  }
  entities.push(entity)
}

///|
fn remove_entity(entities : Array[Entity], id : Int) -> Unit raise EditError {
  guard entity_position(entities, id) is Some(_) else {
    raise MissingEntity(id~)
  }
  let remaining = entities.filter(entity => entity.id != id)
  entities.clear()
  entities.append(remaining)
}

///|
fn replace_entity(
  entities : Array[Entity],
  replacement : Entity,
) -> Unit raise EditError {
  guard entity_position(entities, replacement.id) is Some(position) else {
    raise MissingEntity(id=replacement.id)
  }
  entities[position] = replacement
}

///|
fn set_entity_attribute(
  entities : Array[Entity],
  id : Int,
  index : Int,
  value : Value,
) -> Unit raise EditError {
  guard entity_position(entities, id) is Some(position) else {
    raise MissingEntity(id~)
  }
  if index < 0 || index >= entities[position].arguments.length() {
    raise InvalidAttribute(index~)
  }
  let entity = entities[position]
  let arguments = entity.arguments.copy()
  arguments[index] = value
  entities[position] = { ..entity, arguments, }
}

///|
/// Apply a sequence of edits while preserving the model header and entity order.
pub fn apply_patch(model : Model, patch : ModelPatch) -> Model raise EditError {
  if patch.is_empty() {
    raise EmptyPatch
  }
  let entities = model.entities.copy()
  for operation in patch.operations {
    match operation {
      Add(entity) => add_entity(entities, entity)
      Remove(id) => remove_entity(entities, id)
      Replace(entity) => replace_entity(entities, entity)
      SetAttribute(id~, index~, value~) =>
        set_entity_attribute(entities, id, index, value)
    }
  }
  { ..model, entities, }
}

///|
pub fn Model::apply_patch(
  self : Model,
  patch : ModelPatch,
) -> Model raise EditError {
  apply_patch(self, patch)
}

///|
fn patch_from_change(
  change : ModelChange,
  patch : ModelPatch,
) -> Unit raise EditError {
  match change.kind {
    Added =>
      match change.after {
        Some(entity) => patch.add(entity)
        None => raise MissingEntity(id=change.entity_id)
      }
    Removed => patch.remove(change.entity_id)
    Modified =>
      match change.after {
        Some(entity) => patch.replace(entity)
        None => raise MissingEntity(id=change.entity_id)
      }
  }
}

///|
/// Convert a structural diff into a replayable patch.
pub fn patch_between(left : Model, right : Model) -> ModelPatch raise EditError {
  let diff = diff_models(left, right)
  let patch = ModelPatch::new()
  for change in diff.changes {
    patch_from_change(change, patch)
  }
  patch
}

///|
pub fn patch_summary(patch : ModelPatch) -> String {
  "operations=" +
  patch.operation_count().to_string() +
  " added=" +
  patch.added.to_string() +
  " removed=" +
  patch.removed.to_string() +
  " replaced=" +
  patch.replaced.to_string() +
  " attributes=" +
  patch.changed_attributes.to_string()
}

///|
fn reverse_operation(operation : EntityPatch, before : Model) -> EntityPatch? {
  match operation {
    Add(entity) => Some(Remove(entity.id))
    Remove(id) => before.index().entity(id).map(entity => Add(entity))
    Replace(entity) => before.index().entity(entity.id).map(old => Replace(old))
    SetAttribute(id~, index~, ..) =>
      match before.index().entity(id) {
        Some(entity) =>
          entity.arguments
          .get(index)
          .map(value => SetAttribute(id~, index~, value~))
        None => None
      }
  }
}

///|
/// Build a patch that reverts `patch` against the original model.
pub fn reverse_patch(
  before : Model,
  patch : ModelPatch,
) -> ModelPatch raise EditError {
  let result = ModelPatch::new()
  for operation in patch.operations.rev() {
    match reverse_operation(operation, before) {
      Some(reverse) => result.push(reverse)
      None => raise MissingEntity(id=operation.id())
    }
  }
  result
}

///|
pub fn patch_round_trip(before : Model, after : Model) -> Bool raise EditError {
  let forward = patch_between(before, after)
  let changed = apply_patch(before, forward)
  changed.entities.length() == after.entities.length() &&
  diff_models(changed, after).is_empty()
}

///|
pub fn patch_descriptions(patch : ModelPatch) -> Array[String] {
  patch.operations.map(operation => operation.description())
}

///|
pub fn model_with_entity(
  model : Model,
  entity : Entity,
) -> Model raise EditError {
  let patch = ModelPatch::new()
  patch.add(entity)
  apply_patch(model, patch)
}

///|
pub fn model_without_entity(model : Model, id : Int) -> Model raise EditError {
  let patch = ModelPatch::new()
  patch.remove(id)
  apply_patch(model, patch)
}