///|
/// Creates an empty multi-resource lease table.
pub fn LeaseTable::new() -> LeaseTable {
  { states: [], events: [], accepted_commands: 0, rejected_commands: 0 }
}

///|
fn clone_states(states : Array[LeaseState]) -> Array[LeaseState] {
  states.map(fn(state) { state })
}

///|
fn clone_events(events : Array[LeaseEvent]) -> Array[LeaseEvent] {
  events.map(fn(event) { event })
}

///|
/// Returns the state for a resource.
pub fn LeaseTable::get(self : LeaseTable, resource : String) -> LeaseState? {
  for state in self.states {
    if state.resource == resource {
      return Some(state)
    }
  }
  None
}

///|
/// Returns all leases active at an explicit logical time.
pub fn LeaseTable::active_leases(self : LeaseTable, now : Int) -> Array[Lease] {
  let result : Array[Lease] = []
  for state in self.states {
    match state.active_lease(now) {
      Some(lease) => result.push(lease)
      None => ()
    }
  }
  result
}

///|
/// Applies one resource command and records accepted transitions.
pub fn LeaseTable::apply(
  self : LeaseTable,
  resource : String,
  command : LeaseCommand,
  now : Int,
) -> TableDecision {
  let states = clone_states(self.states)
  let events = clone_events(self.events)
  let mut target = LeaseState::new(resource)
  let mut found = false
  let mut target_index = -1
  for index, state in states {
    if state.resource == resource {
      target = state
      found = true
      target_index = index
      break
    }
  }
  let decision = target.apply(command, now)
  if decision.accepted {
    if found {
      states[target_index] = decision.state
    } else {
      states.push(decision.state)
    }
    match decision.event {
      Some(event) => events.push(event)
      None => ()
    }
  }
  let accepted_delta = if decision.accepted { 1 } else { 0 }
  let rejected_delta = if decision.accepted { 0 } else { 1 }
  let table : LeaseTable = {
    states,
    events,
    accepted_commands: self.accepted_commands + accepted_delta,
    rejected_commands: self.rejected_commands + rejected_delta,
  }
  { table, decision }
}

///|
/// Applies commands in input order and preserves every decision.
pub fn LeaseTable::apply_batch(
  self : LeaseTable,
  commands : Array[TimedCommand],
) -> BatchResult {
  let mut table = self
  let decisions : Array[LeaseDecision] = []
  let mut accepted_count = 0
  let mut rejected_count = 0
  for command in commands {
    let result = table.apply(command.resource, command.command, command.at)
    table = result.table
    decisions.push(result.decision)
    if result.decision.accepted {
      accepted_count = accepted_count + 1
    } else {
      rejected_count = rejected_count + 1
    }
  }
  { table, decisions, accepted: accepted_count, rejected: rejected_count }
}

///|
/// Reaps every expired stored lease in deterministic table order.
pub fn LeaseTable::reap_expired(self : LeaseTable, now : Int) -> BatchResult {
  let commands : Array[TimedCommand] = []
  for state in self.states {
    match state.lease {
      Some(lease) =>
        if !lease.is_active(now) {
          commands.push({ resource: state.resource, command: Reap, at: now })
        }
      None => ()
    }
  }
  self.apply_batch(commands)
}