///|
/// Introspection over the expression tree: a handwritten `Show` rendering
/// the documented operator form, its `to_string` exposing that rendering as a
/// `String` (shared later by `LazyFrame::explain`'s plan printer), and the thin
/// handle-level accessors (`children` / `referenced_columns` / `output_name`)
/// that delegate to the AST analysis living beside the tree in `internal/ir`.
/// All are total — they walk shape, nothing can fail.

///|
/// Render the documented operator form, e.g.
/// `(col(revenue) - col(cost)) as profit`: columns as `col(name)`, bare
/// literals (strings quoted, the null literal as `null`), binary nodes
/// parenthesised infix, prefix `(-e)` / `(not e)`, the remaining unary
/// probes and `Agg` / `Cast` as postfix method calls, `Alias` as
/// `expr as name`, `Ternary` mirroring its `when(c).then(a).otherwise(b)`
/// construction route, a `Map` as `map("label", [inputs])`, and a
/// `LitSeries` as `lit_series("name", len)`. Handwritten because
/// `derive(Show)` is deprecated — and the derived variant form would leak
/// construction-route noise into `explain` output anyway.
pub impl Show for Expr with fn output(self, logger) {
  write_expr(logger, self)
}

///|
pub extend Expr with Show::{to_string, output}

///|
/// Handwritten `@debug.Debug`: the `@ir.ExprNode` `Map` / `MapBatches` closures
/// have no `Debug`, so neither the AST node nor the `Expr` wrapping it can
/// `derive` one. The debug form delegates to
/// the `Show` rendering (`to_string`), which already presents every variant —
/// `Map` by its `(label, inputs)`, the closure opaque — in the documented
/// operator form. Debug output surfaces in `debug_inspect` and in the failure
/// message of any assertion over something that embeds an expression — the
/// handle itself has no equality to fail. `Repr` has no public constructor, so
/// delegating to an existing `Debug` (here `String`'s) is the one way to
/// build one.
pub impl @debug.Debug for Expr with fn to_repr(self) {
  @debug.Debug::to_repr(self.to_string())
}

///|
pub extend Expr with Debug::{to_repr}

///|
/// The immediate sub-expressions of a node, left to right, as opaque `Expr`
/// handles — the wrapper-level face of `@ir.ExprNode::children`, which is the
/// single place that encodes the tree's recursive shape. A new variant declares
/// its child structure once, in `ExprNode`, and every walk takes its *descent*
/// from there: this handle-level face, the `internal/ir` analyses behind
/// `referenced_columns` / `output_name`, and the lazy optimizer's `row_stable` /
/// `group_cell_stable` (which walk the raw `@ir.ExprNode` children directly).
/// What a variant does not inherit is what those walks *decide* about it — the
/// naming and pushdown analyses classify variant by variant, under exhaustive
/// `match`es that stop compiling until the new one is classified. Total — it only
/// inspects shape.
#doc(hidden)
#internal(engine, "MoonFrame execution engine API")
pub fn Expr::children(self : Expr) -> Array[Expr] {
  // The AST's shape lives on `@ir.ExprNode::children`; wrap each raw child back
  // into the opaque `Expr` this returns, so a caller walks the handle.
  self.node.children().map(Expr::of)
}

///|
/// Every column name the tree references, as a set — the handle-level face of
/// `@ir.ExprNode::referenced_columns` (the analysis itself lives in
/// `internal/ir`, beside the AST). A `Ternary`'s condition counts. Total.
#doc(hidden)
#internal(engine, "MoonFrame execution engine API")
pub fn Expr::referenced_columns(self : Expr) -> @set.Set[String] {
  self.node.referenced_columns()
}

///|
/// The output-column name this expression produces (Polars' naming rule: an
/// alias wins, else the leftmost column reference, else `"literal"`) — the
/// handle-level face of `@ir.ExprNode::output_name`. Total.
#doc(hidden)
#internal(engine, "MoonFrame execution engine API")
pub fn Expr::output_name(self : Expr) -> String {
  self.node.output_name()
}

///|
/// A pending step for the iterative `write_expr`: either a literal string
/// fragment to emit, a sub-expression still to expand, or one of the three leaf
/// writers that take more than a plain string (`@literal.format_scalar`
/// for a `Lit`, a `Cast` target, a `Str` suffix).
priv enum WriteTask {
  Frag(String)
  Node(Expr)
  ScalarLit(@types.Scalar)
  CastTarget(@types.DataType)
  StrSuffix(@ir.StrOp)
}

///|
/// Renderer behind `Show`, driven by an explicit heap stack instead of the call
/// stack so a deeply nested tree can't overflow and abort (the same discipline
/// as `@ir.ExprNode::referenced_columns`). Every composite form is
/// self-delimiting (parentheses or
/// a closing `)` of a method call), so a node expands into a fixed sequence of
/// fragments and child placeholders; the emitted text is byte-for-byte the
/// recursive form's.
fn write_expr(logger : &Logger, expr : Expr) -> Unit {
  let stack : Array[WriteTask] = [Node(expr)]
  for ;; {
    match stack.pop() {
      None => break
      Some(task) =>
        match task {
          Frag(s) => logger.write_string(s)
          ScalarLit(v) => logger.write_string(@literal.format_scalar(v))
          CastTarget(t) => t.output(logger)
          StrSuffix(op) => write_str_suffix(logger, op)
          Node(e) => push_write_tasks(stack, e)
        }
    }
  }
}

///|
/// Expand one node into its fragment / child sequence, pushed so the parts emit
/// left to right (built forward, then pushed in reverse so the first pops
/// first). The per-variant shapes mirror the recursive renderer exactly.
fn push_write_tasks(stack : Array[WriteTask], expr : Expr) -> Unit {
  let tasks : Array[WriteTask] = match expr.node {
    // `col(name)` and `Alias`'s ` as name` render the name UNQUOTED, unlike a
    // string literal (`@literal.format_scalar` quotes those), so
    // `col("x")` stays distinct
    // from `lit_str("x")` in a plan line. A name containing literal parens can
    // therefore read paren-ambiguously (`col(a)b)`); that is display-only and
    // accepted — `explain` output is diagnostic, not re-parsed.
    Col(name) => [Frag("col(\{@text.escape_debug(name)})")]
    Lit(value) => [ScalarLit(value)]
    // The embedded data is opaque to the rendering, so a `LitSeries` shows its
    // name (quoted / escaped like a string literal) and length —
    // `lit_series("flag", 4)` — enough to read a plan line without dumping
    // every cell.
    LitSeries(s) =>
      [Frag("lit_series(\"\{@text.escape_debug(s.name())}\", \{s.len()})")]
    // A `Node` holds the opaque `Expr`, so every AST child is wrapped with
    // `Expr::of` before it is pushed for expansion.
    Binary(op, l, r) =>
      [
        Frag("("),
        Node(Expr::of(l)),
        Frag(" \{binop_symbol(op)} "),
        Node(Expr::of(r)),
        Frag(")"),
      ]
    Unary(op, e) =>
      match op {
        Neg => [Frag("(-"), Node(Expr::of(e)), Frag(")")]
        Not => [Frag("(not "), Node(Expr::of(e)), Frag(")")]
        IsNull => [Node(Expr::of(e)), Frag(".is_null()")]
        IsNotNull => [Node(Expr::of(e)), Frag(".is_not_null()")]
        IsNan => [Node(Expr::of(e)), Frag(".is_nan()")]
        IsNotNan => [Node(Expr::of(e)), Frag(".is_not_nan()")]
        Abs => [Node(Expr::of(e)), Frag(".abs()")]
        Floor => [Node(Expr::of(e)), Frag(".floor()")]
        Ceil => [Node(Expr::of(e)), Frag(".ceil()")]
        Sign => [Node(Expr::of(e)), Frag(".sign()")]
        Round(0) => [Node(Expr::of(e)), Frag(".round()")]
        Round(d) => [Node(Expr::of(e)), Frag(".round(decimals=\{d})")]
      }
    Agg(op, e) => [Node(Expr::of(e)), Frag(agg_suffix(op))]
    Str(op, e) => [Node(Expr::of(e)), StrSuffix(op)]
    Cast(e, target) =>
      [Node(Expr::of(e)), Frag(".cast("), CastTarget(target), Frag(")")]
    Alias(e, name) =>
      [Node(Expr::of(e)), Frag(" as \{@text.escape_debug(name)}")]
    Ternary(c, t, f) =>
      [
        Frag("when("),
        Node(Expr::of(c)),
        Frag(").then("),
        Node(Expr::of(t)),
        Frag(").otherwise("),
        Node(Expr::of(f)),
        Frag(")"),
      ]
    // Postfix method-call form, mirroring the construction route — the
    // operand appears once, so a chained coalesce renders linearly.
    FillNull(o, v) =>
      [Node(Expr::of(o)), Frag(".fill_null("), Node(Expr::of(v)), Frag(")")]
    FillNan(o, v) =>
      [Node(Expr::of(o)), Frag(".fill_nan("), Node(Expr::of(v)), Frag(")")]
    // `col(a).is_in([1, 2])`: the operand, then each set member rendered as a
    // literal (`format_scalar`, like a `Lit`) inside `[...]`.
    IsIn(operand, members) => {
      let ts : Array[WriteTask] = [Node(Expr::of(operand)), Frag(".is_in([")]
      for i in 0.. 0 {
          ts.push(Frag(", "))
        }
        ts.push(ScalarLit(members[i]))
      }
      ts.push(Frag("])"))
      ts
    }
    // `col(a).is_between(lo, hi)` — the operand once, then both bounds.
    // A non-default `closed` renders as the named argument that builds it.
    IsBetween(x, lo, hi, closed) => {
      let ts : Array[WriteTask] = [
        Node(Expr::of(x)),
        Frag(".is_between("),
        Node(Expr::of(lo)),
        Frag(", "),
        Node(Expr::of(hi)),
      ]
      match closed {
        @types.ClosedInterval::Both => ()
        @types.ClosedInterval::Left => ts.push(Frag(", closed=Left"))
        @types.ClosedInterval::Right => ts.push(Frag(", closed=Right"))
        @types.ClosedInterval::None => ts.push(Frag(", closed=None"))
      }
      ts.push(Frag(")"))
      ts
    }
    // The closure is opaque, so a `Map` renders by its label and inputs:
    // `map("label", [in1, in2])`. The label is quoted / escaped like a string
    // literal so it stays on one unambiguous line.
    Map(label, inputs, _) => {
      let ts : Array[WriteTask] = [
        Frag("map(\"\{@text.escape_debug(label)}\", ["),
      ]
      for i in 0.. 0 {
          ts.push(Frag(", "))
        }
        ts.push(Node(Expr::of(inputs[i])))
      }
      ts.push(Frag("])"))
      ts
    }
    // Like `Map`, as `map_batches("label", [inputs])`, and — when the node is
    // flagged as a reduction — a trailing `, returns_scalar=true` so the two
    // forms render distinctly.
    MapBatches(label, inputs, returns_scalar, _) => {
      let ts : Array[WriteTask] = [
        Frag("map_batches(\"\{@text.escape_debug(label)}\", ["),
      ]
      for i in 0.. 0 {
          ts.push(Frag(", "))
        }
        ts.push(Node(Expr::of(inputs[i])))
      }
      ts.push(
        Frag(if returns_scalar { "], returns_scalar=true)" } else { "])" }),
      )
      ts
    }
  }
  for i = tasks.length() - 1; i >= 0; i = i - 1 {
    stack.push(tasks[i])
  }
}

///|
/// Infix symbol for a binary tag — the operator where one exists (`&` / `|`
/// for the Kleene connectives), and the conventional mathematical glyph where
/// the builder is a method: `<` for `.lt()`, and `//` / `%` / `**` for
/// `.floor_div()` / `.modulo()` / `.pow()`, none of which MoonBit spells as an
/// operator. Display syntax, not a call site to copy.
fn binop_symbol(op : @ir.BinOp) -> String {
  match op {
    Add => "+"
    Sub => "-"
    Mul => "*"
    Div => "/"
    FloorDiv => "//"
    Mod => "%"
    Pow => "**"
    Eq => "=="
    Ne => "!="
    Lt => "<"
    Le => "<="
    Gt => ">"
    Ge => ">="
    And => "&"
    Or => "|"
  }
}

///|
/// Postfix method spelling for an aggregation tag.
fn agg_suffix(op : @ir.AggOp) -> String {
  match op {
    Sum => ".sum()"
    Mean => ".mean()"
    Min => ".min()"
    Max => ".max()"
    Count => ".count()"
    Std => ".std()"
    Var => ".variance()"
    Median => ".median()"
    NUnique => ".n_unique()"
    First => ".first()"
    Last => ".last()"
  }
}

///|
/// Postfix method rendering for a string-namespace tag, written straight to
/// `logger` (unlike `agg_suffix`'s fixed string) because the parameterised
/// operations carry literal arguments that are quoted and escaped like a
/// `lit_str` — `.str_contains("foo")`, `.str_replace("a", "b")` — so a plan
/// line stays unambiguous and on one line.
fn write_str_suffix(logger : &Logger, op : @ir.StrOp) -> Unit {
  match op {
    ToUppercase => logger.write_string(".str_to_uppercase()")
    ToLowercase => logger.write_string(".str_to_lowercase()")
    StripChars => logger.write_string(".str_strip_chars()")
    LenChars => logger.write_string(".str_len_chars()")
    Contains(p) => write_str_call(logger, "str_contains", [p])
    StartsWith(p) => write_str_call(logger, "str_starts_with", [p])
    EndsWith(p) => write_str_call(logger, "str_ends_with", [p])
    Replace(a, b) => write_str_call(logger, "str_replace", [a, b])
    ReplaceAll(a, b) => write_str_call(logger, "str_replace_all", [a, b])
    Reverse => logger.write_string(".str_reverse()")
    // The width renders bare, the fill as a `'x'` char literal (escaped like a
    // string so a control or quote character stays on one line).
    PadStart(width, fill) =>
      logger.write_string(
        ".str_pad_start(\{width}, '\{@text.escape_debug(fill.to_string())}')",
      )
    PadEnd(width, fill) =>
      logger.write_string(
        ".str_pad_end(\{width}, '\{@text.escape_debug(fill.to_string())}')",
      )
    // The regex forms render as the spelling that builds them: the same
    // method with `literal=false`.
    ContainsRegex(p) =>
      logger.write_string(
        ".str_contains(\"\{@text.escape_debug(p)}\", literal=false)",
      )
    ReplaceRegex(a, b) =>
      logger.write_string(
        ".str_replace(\"\{@text.escape_debug(a)}\", \"\{@text.escape_debug(b)}\", literal=false)",
      )
    ReplaceAllRegex(a, b) =>
      logger.write_string(
        ".str_replace_all(\"\{@text.escape_debug(a)}\", \"\{@text.escape_debug(b)}\", literal=false)",
      )
    // The pattern renders quoted, the group index bare.
    Extract(pattern, group) =>
      logger.write_string(
        ".str_extract(\"\{@text.escape_debug(pattern)}\", \{group})",
      )
    CountMatches(p) => write_str_call(logger, "str_count_matches", [p])
    // The offset renders bare; the length only when present.
    Slice(offset, length) =>
      match length {
        None => logger.write_string(".str_slice(\{offset})")
        Some(n) => logger.write_string(".str_slice(\{offset}, \{n})")
      }
    LenBytes => logger.write_string(".str_len_bytes()")
    // The custom strip renders its charset as a quoted argument, so it stays
    // distinct from the default `.str_strip_chars()`.
    StripCharsCustom(chars) =>
      write_str_call(logger, "str_strip_chars", [chars])
    // The separator renders quoted, the index bare.
    SplitGet(sep, index) =>
      logger.write_string(
        ".str_split_get(\"\{@text.escape_debug(sep)}\", \{index})",
      )
    ZFill(width) => logger.write_string(".str_zfill(\{width})")
  }
}

///|
/// Render one parameterised string-op call `.name("arg", …)`, each argument
/// quoted and `escape_debug`-escaped exactly like a rendered `lit_str`
/// (`@literal.format_scalar`), so a pattern containing a `"` or a
/// control character stays on one unambiguous line.
fn write_str_call(
  logger : &Logger,
  name : String,
  args : Array[String],
) -> Unit {
  logger.write_string(".")
  logger.write_string(name)
  logger.write_string("(")
  logger.write_string(args.map(a => "\"\{@text.escape_debug(a)}\"").join(", "))
  logger.write_string(")")
}