///|
pub(all) struct TableExample {
  before : Table
  after : Table
}

///|
pub(all) struct TableSearchOptions {
  max_depth : Int32
  max_states : Int32
  max_evaluations : Int32
  max_candidates : Int32
}

///|
pub fn default_table_options() -> TableSearchOptions {
  { max_depth: 4, max_states: 128, max_evaluations: 2000, max_candidates: 8, }
}

///|
pub(all) struct TableSearchReport {
  candidates : Array[Pipeline]
  truncated : Bool
  status : String
  states : Int32
  evaluations : Int32
  rejected : Int32
  row_searches : Int32
}

///|
priv struct TableState {
  tables : Array[Table]
  steps : Array[TableStep]
  priority : Int32
  serial : Int32
  key : String
}

///|
fn tables_key(tables : Array[Table]) -> String {
  Json::array(tables.map(table_json)).stringify()
}

///|
fn projection_step(tables : Array[Table], targets : Array[Table]) -> TableStep? {
  if targets.any(t => t.columns != targets[0].columns) ||
    tables.any(t => t.columns != tables[0].columns) {
    return None
  }
  if tables[0].rows.length() != targets[0].rows.length() {
    return None
  }
  let names : Array[String] = []
  for j in 0.. Array[TableStep] {
  let result : Array[TableStep] = [Transpose, PromoteHeaders, DemoteHeaders]
  let wanted : Map[String, Bool] = Map([])
  for row in target.rows {
    for value in row {
      wanted[value] = true
    }
  }
  let useful : Array[String] = []
  let nonempty : Array[String] = []
  for i, col in table.columns {
    if table.rows.any(row => !row[i].is_empty()) {
      nonempty.push(col)
    }
    if table.rows.any(row => !row[i].is_empty() && wanted.contains(row[i])) {
      useful.push(col)
    }
  }
  for names in [useful, nonempty] {
    if !names.is_empty() && names.length() < table.columns.length() {
      result.push(Select(names, names))
    }
  }
  result.push(
    Filter(Not(All(Array::makei(table.columns.length(), i => Empty(Field(i)))))),
  )
  if !table.rows.is_empty() {
    result.push(DropRows(1))
  }
  result.push(FillDown(table.columns))
  result.push(Deduplicate(table.columns))
  for col in table.columns {
    result.push(FillDown([col]))
    result.push(Deduplicate([col]))
    for sep in separators {
      let i = column_index(table, col) catch { _ => continue }
      if !table.rows.any(row => row[i].contains(sep)) {
        continue
      }
      result.push(Explode(col, sep))
      let count = if table.rows.is_empty() {
        0
      } else {
        table.rows[0][i].split(sep).count()
      }
      if count >= 2 && count <= 8 {
        result.push(
          SplitColumn(col, sep, Array::makei(count, j => "\{col}_\{j}")),
        )
      }
    }
    let i = column_index(table, col) catch { _ => continue }
    result.push(Filter(Not(Empty(Field(i)))))
    result.push(SegmentRows(Empty(Field(i)), false))
    let context_width = target.columns.length() - table.columns.length()
    if context_width > 0 && context_width <= table.columns.length() {
      result.push(
        CarryContext(Empty(Field(i)), table.columns[:context_width].to_owned()),
      )
    }
    if !table.rows.is_empty() {
      let chars = table.rows[0][i].to_array()
      if !chars.is_empty() &&
        chars[0].to_int() < 128 &&
        !(chars[0].is_ascii_alphabetic() || chars[0].is_ascii_digit()) &&
        !chars[0].is_whitespace() {
        result.push(
          SegmentRows(
            Matches(Field(i), "^" + regex_literal(chars[0].to_string()), false),
            true,
          ),
        )
      }
    }
    let values : Array[String] = []
    for row in table.rows {
      if !values.contains(row[i]) && values.length() < 9 {
        values.push(row[i])
      }
    }
    if values.length() <= 8 {
      values.sort()
      for value in values {
        result.push(Filter(Equals(Field(i), Literal(value))))
        result.push(Filter(Not(Equals(Field(i), Literal(value)))))
      }
    }
  }
  let width = table.columns.length()
  for n in 2..<9 {
    result.push(PackRows(n, false))
    result.push(PackRows(n, true))
  }
  for n in [target.columns.length(), target.columns.length() / width] {
    if n > 8 && n <= 64 {
      result.push(PackRows(n, false))
      result.push(PackRows(n, true))
    }
  }
  for i in 0.. wanted.contains(row[i] + sep + row[j])) {
          result.push(
            CombineColumns(
              [table.columns[i], table.columns[j]],
              sep,
              "merged_\{i}_\{j}",
            ),
          )
        }
      }
    }
  }
  for n in 0..