// Event hooks: the points where a caller can observe or adjust what the unit of work
// is about to do. SQLAlchemy dispatches these through `event.listen(...)` on a
// mapper or a session, with a different signature per event. MoonBit has one closure
// shape, so there is one context and every event hands it the same thing — the table,
// the row being written, and, for an `After*` hook, the statement that ran.

///|
/// The point in the unit of work a listener fires at. The `Before*` write hooks see
/// the row before it is rendered and may rewrite its columns; the `After*` hooks see
/// the statement that ran. `BeforeFlush` / `AfterFlush` bracket the whole flush, and
/// `BeforeCommit` / `AfterCommit` / `AfterRollback` bracket the transaction.
///
/// The names follow SQLAlchemy's `before_insert`, `after_insert` and friends, which
/// is why they are read as the phase they fire in rather than as statements.
#derive(Eq)
pub(all) enum HookEvent {
  BeforeInsert
  AfterInsert
  BeforeUpdate
  AfterUpdate
  BeforeDelete
  AfterDelete
  BeforeFlush
  AfterFlush
  BeforeCommit
  AfterCommit
  AfterRollback
}

///|
/// What a listener is told. `columns` is the row the unit of work is about to write,
/// as the model's `to_columns` projected it — and it is mutable, so a `Before*` hook
/// can stamp a timestamp, normalise a value, or drop a column, and the flush renders
/// what the hook left behind. For a delete it holds the primary key that addresses the
/// row. `sql` and `params` carry the statement that ran, filled in for `After*` hooks
/// and empty before it.
pub struct HookContext {
  event : HookEvent
  table : String
  mut columns : Array[(String, @moondb.Value)]
  mut sql : String
  mut params : Array[@moondb.Value]
}

///|
/// The context handed to one listener. Hooks outside a row-level statement — a flush
/// or a transaction boundary — carry no table and no columns.
pub fn HookContext::new(
  event : HookEvent,
  table~ : String = "",
  columns~ : Array[(String, @moondb.Value)] = [],
) -> HookContext {
  { event, table, columns, sql: "", params: [], }
}

///|
/// Replace a column's value in the row being written, or append it when the row does
/// not carry the column yet. This is how a `BeforeInsert` hook fills a column in —
/// SQLAlchemy's idiom of setting an attribute on the instance in `before_insert`.
pub fn HookContext::set_column(
  self : HookContext,
  name : String,
  value : @moondb.Value,
) -> Unit {
  for i in 0.. Unit {
  self.columns = columns
}

///|
/// The value the row carries for `name`, or `None`.
pub fn HookContext::column(self : HookContext, name : String) -> @moondb.Value? {
  for pair in self.columns {
    if pair.0 == name {
      return Some(pair.1)
    }
  }
  None
}

///|
/// Register a listener for `event` on this session. The listener runs for every
/// statement of that kind this session emits; narrowing it to one table is a check on
/// `ctx.table`, which is what a mapper-level listener in SQLAlchemy does by being
/// registered on that mapper.
///
/// Listeners run in registration order. A `Before*` listener that raises aborts the
/// flush before the statement is sent; nothing already executed is replayed, so a
/// rollback leaves the session where a failed flush would.
pub fn Session::on(
  self : Session,
  event : HookEvent,
  handler : (HookContext) -> Unit raise @moondb.DbError,
) -> Unit {
  self.hooks.push((event, handler))
}

///|
/// How many listeners are registered for `event`.
pub fn Session::listener_count(self : Session, event : HookEvent) -> Int32 {
  let mut n = 0
  for hook in self.hooks {
    if hook.0 == event {
      n = n + 1
    }
  }
  n
}

///|
/// Run every listener registered for `event`, in registration order.
fn Session::fire(
  self : Session,
  ctx : HookContext,
) -> Unit raise @moondb.DbError {
  for hook in self.hooks {
    if hook.0 == ctx.event {
      (hook.1)(ctx)
    }
  }
}

///|
/// Remove every listener, so a session can be reused with a fresh set of hooks.
pub fn Session::off_all(self : Session) -> Unit {
  self.hooks.clear()
}

///|
pub extend HookEvent with Eq::{not_equal, equal}