///|
priv struct Ranked {
  rank : Int32
  row : @types.Value
}

///|
fn output_name(output : @xml.Node) -> String {
  let name = output.attr("name")
  if name != "" {
    name
  } else {
    output.attr("id")
  }
}

///|
fn output_priority(
  output : @xml.Node,
  value : @types.Value,
  ctx : Map[String, @types.Value],
) -> Int32 raise @types.EngineError {
  match output.child("outputValues") {
    Some(allowed) => @eval.allowed_rank(allowed.content("text"), value, ctx)
    None => -1
  }
}

///|
fn first_value(
  outputs : Array[@xml.Node],
  row : Map[String, @types.Value],
) -> @types.Value {
  row.get(output_name(outputs[0])).unwrap_or(Null)
}

///|
fn row_value(
  outputs : Array[@xml.Node],
  row : Map[String, @types.Value],
) -> @types.Value {
  if outputs.length() == 1 {
    row.get(output_name(outputs[0])).unwrap_or(Null)
  } else {
    Context(row)
  }
}

///|
fn default_row(
  outputs : Array[@xml.Node],
  ctx : Map[String, @types.Value],
) -> @types.Value raise @types.EngineError {
  let row = Map([])
  for output in outputs {
    let value = match output.child("defaultOutputEntry") {
      Some(entry) => @eval.feel_eval(entry.content("text"), ctx~)
      None => Null
    }
    row[output_name(output)] = value
  }
  row_value(outputs, row)
}

///|
fn aggregate(
  name : String,
  matches : Array[@types.Value],
) -> @types.Value raise @types.EngineError {
  if name == "COUNT" {
    return Number(@types.decimal(matches.length().to_string()))
  }
  if matches.is_empty() {
    return Null
  }
  let mut n = match matches[0] {
    Number(n) => n
    _ => raise @types.fail("E_TABLE_AGGREGATION", "numeric output required")
  }
  for i = 1; i < matches.length(); i = i + 1 {
    let m = match matches[i] {
      Number(m) => m
      _ => raise @types.fail("E_TABLE_AGGREGATION", "numeric output required")
    }
    match name {
      "SUM" | "C+" => n = n.add(m)
      "MIN" | "C<" => if m.compare(n) < 0 { n = m }
      "MAX" | "C>" => if m.compare(n) > 0 { n = m }
      _ => raise @types.fail("E_TABLE_AGGREGATION", name)
    }
  }
  Number(n)
}

///|
fn table(
  node : @xml.Node,
  ctx : Map[String, @types.Value],
) -> @types.Value raise @types.EngineError {
  let policy = if node.attr("hitPolicy") == "" {
    "UNIQUE"
  } else {
    node.attr("hitPolicy")
  }
  let collect = policy == "COLLECT" ||
    policy == "C+" ||
    policy == "C<" ||
    policy == "C>" ||
    policy == "C#"
  if !(collect ||
    ["UNIQUE", "ANY", "FIRST", "PRIORITY", "RULE ORDER", "OUTPUT ORDER"].contains(
      policy,
    )) {
    raise @types.fail("E_HIT_POLICY_UNSUPPORTED", policy)
  }
  let inputs = node.all("input")
  let outputs = node.all("output")
  if outputs.is_empty() {
    raise @types.fail("E_TABLE_SHAPE", "no output columns")
  }
  let values = []
  for input in inputs {
    let expr = match input.child("inputExpression") {
      Some(n) => n
      None => raise @types.fail("E_TABLE_SHAPE", "missing inputExpression")
    }
    let v = @eval.feel_eval(expr.content("text"), ctx~)
    if input.child("inputValues") is Some(allowed) {
      if !@eval.unary_test(allowed.content("text"), v, ctx) {
        raise @types.fail("E_INPUT_VALUES", "input violates allowed values")
      }
    }
    values.push(v)
  }
  let matches = []
  for rule in node.all("rule") {
    let tests = rule.all("inputEntry")
    let entries = rule.all("outputEntry")
    if tests.length() != inputs.length() || entries.length() != outputs.length() {
      raise @types.fail("E_TABLE_SHAPE", "rule column count mismatch")
    }
    let mut matched = true
    for i, test_node in tests {
      if !@eval.unary_test(test_node.content("text"), values[i], ctx) {
        matched = false
      }
    }
    if !matched {
      continue
    }
    let row = Map([])
    for i, entry in entries {
      let v = @eval.feel_eval(entry.content("text"), ctx~)
      if outputs[i].child("outputValues") is Some(allowed) {
        if !@eval.unary_test(allowed.content("text"), v, ctx) {
          raise @types.fail("E_OUTPUT_VALUES", "output violates allowed values")
        }
      }
      row[output_name(outputs[i])] = v
    }
    matches.push(row)
    if policy == "FIRST" {
      break
    }
  }
  if matches.is_empty() &&
    outputs.iter().any(o => o.child("defaultOutputEntry") is Some(_)) {
    return default_row(outputs, ctx)
  }
  if collect {
    if outputs.length() != 1 {
      if node.attr("aggregation") != "" {
        raise @types.fail(
          "E_TABLE_AGGREGATION", "aggregation requires single output",
        )
      }
    }
    let rows = matches.map(row => row_value(outputs, row))
    let agg = if node.attr("aggregation") != "" {
      node.attr("aggregation")
    } else {
      match policy {
        "C+" => "SUM"
        "C<" => "MIN"
        "C>" => "MAX"
        "C#" => "COUNT"
        _ => ""
      }
    }
    if agg == "" {
      return List(rows)
    }
    if outputs.length() != 1 {
      raise @types.fail(
        "E_TABLE_AGGREGATION", "aggregation requires single output",
      )
    }
    return aggregate(agg, rows)
  }
  if matches.is_empty() {
    if policy == "OUTPUT ORDER" {
      for output in outputs {
        if output.child("defaultOutputEntry") is Some(_) {
          return List([default_row(outputs, ctx)])
        }
      }
      return List([])
    }
    return default_row(outputs, ctx)
  }
  if policy == "RULE ORDER" || policy == "OUTPUT ORDER" {
    let rows = matches.map(row => row_value(outputs, row))
    if policy == "OUTPUT ORDER" {
      let ranked = []
      for row in matches {
        let rank = output_priority(outputs[0], first_value(outputs, row), ctx)
        ranked.push({ rank, row: row_value(outputs, row), })
      }
      // Stable insertion sort: priority order, then rule order for ties.
      for i = 1; i < ranked.length(); i = i + 1 {
        let item = ranked[i]
        let mut j = i - 1
        while j >= 0 && ranked[j].rank > item.rank {
          ranked[j + 1] = ranked[j]
          j = j - 1
        }
        ranked[j + 1] = item
      }
      return List(ranked.map(item => item.row))
    }
    return List(rows)
  }
  if policy == "PRIORITY" {
    let mut best = matches[0]
    let mut best_rank = output_priority(
      outputs[0],
      first_value(outputs, matches[0]),
      ctx,
    )
    for i = 1; i < matches.length(); i = i + 1 {
      let rank = output_priority(
        outputs[0],
        first_value(outputs, matches[i]),
        ctx,
      )
      if best_rank < 0 || (rank >= 0 && rank < best_rank) {
        best = matches[i]
        best_rank = rank
      }
    }
    return row_value(outputs, best)
  }
  if policy == "UNIQUE" && matches.length() > 1 {
    raise @types.fail("E_HIT_UNIQUE", "multiple rules matched")
  }
  if policy == "ANY" &&
    !matches
    .iter()
    .all(row => row_value(outputs, row).equals(row_value(outputs, matches[0]))) {
    raise @types.fail("E_HIT_ANY", "matched outputs differ")
  }
  row_value(outputs, matches[0])
}