///|
/// Lazy formula expression over target facade handles. Materializes to a single
/// `Derived[T]` via `Expr::derived`; operator chains allocate no incremental cells.
pub(all) struct Expr[T] {
  priv rt : Runtime
  priv eval : () -> T raise Failure
  priv debug : ExprDebug
}

///|
priv enum ExprDebug {
  Source(String?)
  Const(String?)
  Op(String, ExprDebug, ExprDebug)
  Map(ExprDebug)
}

///|
/// Match-all guard: exhaustively lists every variant so the compiler doesn't
/// warn about unused constructors (each variant carries data and IS constructed
/// by one of the `Expr`/`Input::expr`/`EagerDerived::expr` methods).
fn ExprDebug::to_string(self : ExprDebug) -> String {
  match self {
    Source(l) =>
      "Source(" +
      (match l {
        Some(s) => "\"" + s + "\""
        None => "None"
      }) +
      ")"
    Const(l) =>
      "Const(" +
      (match l {
        Some(s) => "\"" + s + "\""
        None => "None"
      }) +
      ")"
    Op(name, left, right) =>
      "Op(" + name + ", " + left.to_string() + ", " + right.to_string() + ")"
    Map(inner) => "Map(" + inner.to_string() + ")"
  }
}

///|
/// Creates a constant expression tied to an explicit runtime.
pub fn[T] Expr::constant(rt : Runtime, value : T, label? : String) -> Expr[T] {
  { rt, eval: () => value, debug: ExprDebug::Const(label) }
}

///|
pub fn[A, B] Expr::map(self : Expr[A], f : (A) -> B raise Failure) -> Expr[B] {
  {
    rt: self.rt,
    eval: () => f((self.eval)()),
    debug: ExprDebug::Map(self.debug),
  }
}

///|
pub fn[A, B, C] Expr::map2(
  left : Expr[A],
  right : Expr[B],
  f : (A, B) -> C raise Failure,
) -> Expr[C] {
  Expr::assert_same_runtime(left, right, "map2")
  {
    rt: left.rt,
    eval: () => f((left.eval)(), (right.eval)()),
    debug: ExprDebug::Op("map2", left.debug, right.debug),
  }
}

///|
fn[A, B] Expr::assert_same_runtime(
  left : Expr[A],
  right : Expr[B],
  op : String,
) -> Unit {
  let left_rt = left.rt.id()
  let right_rt = right.rt.id()
  if left_rt != right_rt {
    abort(
      "Cross-runtime expression: operator " +
      op +
      " composed Runtime " +
      right_rt.to_string() +
      " with Runtime " +
      left_rt.to_string(),
    )
  }
}

///|
/// Materializes this expression as one lazy `Derived` cell on its runtime.
pub fn[T : Eq] Expr::derived(self : Expr[T], label? : String) -> Derived[T] {
  Derived(self.rt, () => (self.eval)(), label?)
}

// ── Source lifts ──

///|
#alias(e)
pub fn[T] Input::expr(self : Input[T]) -> Expr[T] {
  { rt: self.rt, eval: () => self.get(), debug: ExprDebug::Source(self.label) }
}

///|
#alias(e)
pub fn[T] InputField::expr(self : InputField[T]) -> Expr[T] {
  self.input.expr()
}

///|
#alias(e)
pub fn[T] Derived::expr(self : Derived[T]) -> Expr[T] {
  {
    rt: self.rt,
    eval: () => self.get_or_abort(),
    debug: ExprDebug::Source(self.label),
  }
}

///|
#alias(e)
pub fn[T : Eq] ReachableDerived::expr(self : ReachableDerived[T]) -> Expr[T] {
  {
    rt: self.rt,
    eval: () => self.get_or_abort(),
    debug: ExprDebug::Source(self.label),
  }
}

///|
#alias(e)
pub fn[T] EagerDerived::expr(self : EagerDerived[T]) -> Expr[T] {
  { rt: self.rt, eval: () => self.get(), debug: ExprDebug::Source(None) }
}

// ── Operator trait implementations ──

///|
/// Adds two expressions when materialized. The operator chain stays lazy until
/// `Expr::derived`; no incremental cell is allocated for `+` itself. Aborts if
/// the operands belong to different runtimes.
pub impl[T : Add] Add for Expr[T] with fn add(self, other) -> Expr[T] {
  Expr::map2(self, other, (a, b) => a + b)
}

///|
/// Subtracts the right expression from the left when materialized. Stays lazy
/// until `Expr::derived`; no incremental cell is allocated for `-` itself.
/// Aborts if the operands belong to different runtimes.
pub impl[T : Sub] Sub for Expr[T] with fn sub(self, other) -> Expr[T] {
  Expr::map2(self, other, (a, b) => a - b)
}

///|
/// Multiplies two expressions when materialized. Stays lazy until
/// `Expr::derived`; no incremental cell is allocated for `*` itself. Aborts if
/// the operands belong to different runtimes.
pub impl[T : Mul] Mul for Expr[T] with fn mul(self, other) -> Expr[T] {
  Expr::map2(self, other, (a, b) => a * b)
}

///|
/// Divides the left expression by the right when materialized. Stays lazy until
/// `Expr::derived`; no incremental cell is allocated for `/` itself. Aborts if
/// the operands belong to different runtimes.
pub impl[T : Div] Div for Expr[T] with fn div(self, other) -> Expr[T] {
  Expr::map2(self, other, (a, b) => a / b)
}

///|
/// Remainder of dividing the left expression by the right when materialized.
/// Stays lazy until `Expr::derived`; no incremental cell is allocated for `%`
/// itself. Aborts if the operands belong to different runtimes.
pub impl[T : Mod] Mod for Expr[T] with fn mod(self, other) -> Expr[T] {
  Expr::map2(self, other, (a, b) => a % b)
}

///|
/// Negates the expression when materialized. Composes lazily via `Expr::map`;
/// no incremental cell is allocated for unary `-` until `Expr::derived`.
pub impl[T : Neg] Neg for Expr[T] with fn neg(self) -> Expr[T] {
  Expr::map(self, a => -a)
}