// Scaling benchmarks for adversarial query shapes (not part of `moon test`).
//
//   moon run src/bench --release [--target native] [--  []]
//
// For every case and operation the input size doubles; the time per run and the ratio to
// the previous size are printed (about 2x per doubling is linear, about 4x quadratic).
// A size whose run exceeds the time budget stops that operation. Deep shapes may hit the
// host stack limit; run them one at a time (`-- nested_parens`) to isolate a crash.

///|
/// Stop growing an operation once one run takes longer than this (microseconds).
let budget_us : Double = 2000000.0

///|
/// Repeat fast operations until this much time has been measured (microseconds).
let min_total_us : Double = 50000.0

///|
fn time_op(f : () -> Unit raise) -> Double raise {
  let mut runs = 0
  let mut total = 0.0
  while runs == 0 || (total < min_total_us && runs < 1000) {
    let t = @timing.monotonic_clock_start()
    f()
    total += @timing.monotonic_clock_end(t)
    runs += 1
  }
  total / runs.to_double()
}

///|
fn run_op(op : String, sql : String, parsed : @core.Expr) -> Unit raise {
  match op {
    "parse" => ignore(@sqlglot.parse(if sql == "" { gen(parsed) } else { sql }))
    "generate" => ignore(@sqlglot.generate(parsed))
    "pretty" => ignore(@sqlglot.generate(parsed, pretty=true))
    "copy" => ignore(parsed.copy())
    "simplify" => ignore(@optimizer.simplify(parsed.copy()))
    "optimize" => ignore(@optimizer.optimize(parsed.copy()))
    "qualify" => ignore(@optimizer.qualify(parsed.copy()))
    _ => fail("unknown op \{op}")
  }
}

///|
let rule_names : Array[String] = [
  "qualify", "pushdown_projections", "normalize", "unnest_subqueries", "pushdown_predicates",
  "optimize_joins", "eliminate_subqueries", "merge_subqueries", "eliminate_joins",
  "eliminate_ctes", "quote_identifiers", "annotate_types", "canonicalize", "simplify",
]

///|
/// Times each default optimizer rule on `parsed` (applied in sequence, one run each).
fn time_rules(name : String, n : Int, parsed : @core.Expr) -> Unit raise {
  let mut prefix = 0.0
  for i in 0..<@optimizer.rules.length() {
    let rules = @optimizer.rules[:i + 1].to_owned()
    let t = @timing.monotonic_clock_start()
    ignore(@optimizer.optimize(parsed.copy(), rules~))
    let total = @timing.monotonic_clock_end(t)
    println(
      "\{name}\trule:\{rule_names[i]}\t\{n}\t\{fmt_ms(total - prefix)}\t-",
    )
    prefix = total
  }
}

///|
/// Times the steps of `qualify` on `parsed` (one run each).
fn time_qualify_steps(
  name : String,
  n : Int,
  parsed : @core.Expr,
) -> Unit raise {
  let schema = @optimizer.MappingSchema::new()
  let mut e = parsed.copy()
  let step = (label : String, f : () -> @core.Expr raise) => {
    let t = @timing.monotonic_clock_start()
    e = f()
    println(
      "\{name}\tqualify:\{label}\t\{n}\t\{fmt_ms(@timing.monotonic_clock_end(t))}\t-",
    )
  }
  step("traverse_scope", () => {
    ignore(@optimizer.traverse_scope(e))
    e
  })
  step("normalize_identifiers", () => @optimizer.normalize_identifiers(e))
  step("qualify_tables", () => @optimizer.qualify_tables(e))
  step("qualify_columns", () => @optimizer.qualify_columns(e, schema))
  step("quote_identifiers", () => @optimizer.quote_identifiers(e))
  step("validate", () => @optimizer.validate_qualify_columns(e))
}

///|
fn gen(e : @core.Expr) -> String raise {
  @sqlglot.generate(e)
}

///|
fn first_statement(sql : String) -> @core.Expr raise {
  match @sqlglot.parse(sql) {
    [Some(e), ..] => e
    _ => fail("no statement")
  }
}

///|
fn fmt_ms(us : Double) -> String {
  let ms = us / 1000.0
  let r = (ms * 100.0).round() / 100.0
  r.to_string()
}

///|
fn fmt_ratio(r : Double) -> String {
  ((r * 10.0).round() / 10.0).to_string()
}

///|
fn main {
  let args = @env.args()
  // `moon run` passes the program name first; filters follow
  let filters = args.filter(a => {
    !a.has_suffix(".wasm") && !a.has_suffix(".exe") && !a.contains("/")
  })
  let case_filter = filters.get(0).unwrap_or("")
  let op_filter = filters.get(1).unwrap_or("")
  // an optional third argument runs a single size
  let only_size = match filters.get(2) {
    Some(s) => @string.parse_int(s) catch { _ => 0 }
    None => 0
  }
  let flagged : Array[String] = []
  println("case\top\tn\tms/run\tratio")
  for c in cases {
    if case_filter != "" && c.name != case_filter {
      continue
    }
    for
      op in (if op_filter is ("rules" | "qualify" | "qualify_steps") {
        [op_filter]
      } else {
        c.ops
      }) {
      if op_filter != "" && op != op_filter {
        continue
      }
      let mut prev : Double? = None
      let mut last_ratio = 0.0
      for n in (if only_size > 0 { [only_size] } else { c.sizes }) {
        let sql = (c.sql)(n)
        let parsed = try {
          match c.build {
            Some(b) => b(n)
            None => first_statement(sql)
          }
        } catch {
          e => {
            println("\{c.name}\t\{op}\t\{n}\terror\t\{e}")
            break
          }
        }
        if op == "qualify_steps" {
          time_qualify_steps(c.name, n, parsed) catch {
            e => println("\{c.name}\tqualify_steps\t\{n}\terror\t\{e}")
          }
          continue
        }
        if op == "rules" {
          time_rules(c.name, n, parsed) catch {
            e => println("\{c.name}\trules\t\{n}\terror\t\{e}")
          }
          continue
        }
        let us = time_op(() => run_op(op, sql, parsed)) catch {
          e => {
            println("\{c.name}\t\{op}\t\{n}\terror\t\{e}")
            break
          }
        }
        let ratio = match prev {
          Some(p) if p > 0.0 => us / p
          _ => 0.0
        }
        println(
          "\{c.name}\t\{op}\t\{n}\t\{fmt_ms(us)}\t\{if ratio > 0.0 { fmt_ratio(ratio) } else { "-" }}",
        )
        prev = Some(us)
        last_ratio = ratio
        if us > budget_us {
          break
        }
      }
      println("\{c.name}\t\{op}\tdone")
      // Ratios are noisy for tiny timings; flag only clear growth on measurable sizes.
      if last_ratio > 3.0 && prev.unwrap_or(0.0) > 5000.0 {
        flagged.push("\{c.name}/\{op} (x\{fmt_ratio(last_ratio)} per doubling)")
      }
    }
  }
  println("")
  if flagged.is_empty() {
    println("no super-linear growth flagged")
  } else {
    println("super-linear:")
    for f in flagged {
      println("  \{f}")
    }
  }
}