///|
fn condition_cost(c : Condition) -> Int32 {
  match c {
    Empty(_) => 2
    Equals(_, Literal(s)) | Equals(Literal(s), _) =>
      if s.is_empty() {
        3
      } else {
        12 + s.char_length()
      }
    Equals(_, _) => 8
    Matches(_, p, _) => 6 + p.char_length() / 8
    Not(c) => condition_cost(c) + 1
    All(cs) | Any(cs) => cs.fold(init=2, (n, c) => n + condition_cost(c))
  }
}

///|
fn expression_cost(e : Expr) -> Int32 {
  match e {
    Field(_) => 1
    Literal(s) => 40 + s.char_length()
    Trim(e) | Lower(e) | Upper(e) => 2 + expression_cost(e)
    Token(e, _, _) | Run(e, _, _) => 5 + expression_cost(e)
    Slice(e, _, _) => 12 + expression_cost(e)
    DateText(e, _, _) => 8 + expression_cost(e)
    RegexCapture(e, _, _, _) => 7 + expression_cost(e)
    RegexReplace(e, _, r) => 9 + r.char_length() + expression_cost(e)
    Concat(es) => es.fold(init=2, (n, e) => n + expression_cost(e))
    If(c, a, b) =>
      8 + condition_cost(c) + expression_cost(a) + expression_cost(b)
  }
}

///|
/// A declared simplicity/generalization prior, never a probability or proof.
pub fn pipeline_cost(p : Pipeline) -> Int32 {
  p.steps.fold(init=0, (cost, step) => {
    cost +
    (match step {
      Select(_, _) => 3
      FillDown(_) => 4
      Filter(c) => 4 + condition_cost(c)
      Deduplicate(_) => 6
      DropRows(n) => 4 + n
      PackRows(n, _) => 6 + n / 16
      Transpose => 18
      PromoteHeaders | DemoteHeaders => 7
      Unpivot(_, _, _, _) | Fold(_, _, _) | Melt(_, _, _, _, _) => 8
      Pivot(_, _, _, levels) => 12 + levels.length() / 8
      PivotGrid(_, _, _) => 9
      GroupRows(_, _) => 9
      Aggregate(_, measures) => 11 + measures.length()
      SplitColumn(_, _, _) | Explode(_, _) => 7
      CombineColumns(names, sep, _) => 5 + names.length() + sep.char_length()
      SegmentRows(c, _) => 8 + condition_cost(c)
      CarryContext(c, _) => 9 + condition_cost(c)
      MapRows(r) =>
        r.outputs.fold(init=8, (n, o) => n + expression_cost(o.expression))
    })
  })
}

///|
priv struct TargetProfile {
  width : Int32
  height : Int32
  frequencies : Map[String, Int32]
  total : Int32
}

///|
fn target_profile(target : Table) -> TargetProfile {
  let wanted : Map[String, Int32] = Map([])
  let mut total = 0
  for row in target.rows {
    for value in row {
      if value.is_empty() {
        continue
      }
      wanted[value] = wanted.get(value).unwrap_or(0) + 1
      total = total + 1
    }
  }
  {
    width: target.columns.length(),
    height: target.rows.length(),
    frequencies: wanted,
    total,
  }
}

///|
fn table_distance(table : Table, target : TargetProfile) -> Int32 {
  let counts : Map[String, Int32] = Map([])
  let mut present = 0
  let mut cells = 0
  for row in table.rows {
    for value in row {
      if value.is_empty() {
        continue
      }
      cells = cells + 1
      let n = counts.get(value).unwrap_or(0)
      if n < target.frequencies.get(value).unwrap_or(0) {
        present = present + 1
        counts[value] = n + 1
      }
    }
  }
  let shape = (table.columns.length() - target.width).abs() * 4 +
    (table.rows.length() - target.height).abs() *
    20 /
    Int32::max(1, Int32::max(table.rows.length(), target.height))
  let missing = (target.total - present) * 100 / Int32::max(1, target.total)
  let extra = (cells - present) * 20 / Int32::max(1, cells)
  shape + missing + extra
}

///|
fn state_priority(
  tables : Array[Table],
  targets : Array[TargetProfile],
  steps : Array[TableStep],
) -> Int32 {
  let mut distance = 0
  for i, table in tables {
    distance = distance + table_distance(table, targets[i])
  }
  distance * 4 + pipeline_cost({ columns: tables[0].columns, steps, })
}

///|
fn regex_literal(text : String) -> String {
  let out = StringBuilder()
  for c in text {
    if "\\.^$|?*+()[]{}".contains(c.to_string()) {
      out.write_char('\\')
    }
    out.write_char(c)
  }
  out.to_string()
}