// Field level schema inference.
//
// MoonLoom does not ask for a schema, it recovers one from the data it read.
// Inference only reports what the values prove: a column is `int` when every
// present value is an integer and `mixed` as soon as two kinds disagree.

///|
/// Per column summary used by `moonloom schema`, by the planner when it decides
/// whether a comparison can be answered numerically, and by `explain`.
pub(all) struct FieldInfo {
  name : String
  kind : ValueKind
  present : Int
  missing : Int
  distinct : Int
} derive(Eq, Debug)

///|
pub extend FieldInfo with Eq::{not_equal, equal}

///|
pub extend FieldInfo with @debug.Debug::{to_repr}

///|
fn merge_kind(current : ValueKind, next : ValueKind) -> ValueKind {
  if current == next {
    return current
  }
  if current == KindNull {
    return next
  }
  if next == KindNull {
    return current
  }
  let numeric = (current == KindInt || current == KindNum) &&
    (next == KindInt || next == KindNum)
  if numeric {
    return KindNum
  }
  KindMixed
}

///|
/// Summarises every column of a table.
///
/// `distinct` counts the different printed values per column; it is optional
/// because it costs one set per column on wide tables.
pub fn infer_fields(table : Table, distinct? : Bool) -> Array[FieldInfo] {
  let count_distinct = distinct.unwrap_or(true)
  let fields : Array[FieldInfo] = []
  for name in table.columns {
    let mut kind = KindNull
    let mut present = 0
    let mut missing = 0
    let seen : Set[String] = Set([])
    for record in table.records {
      match record.fields.get(name) {
        Some(value) =>
          match value {
            Null => missing += 1
            _ => {
              present += 1
              kind = merge_kind(kind, value.kind())
              if count_distinct {
                seen.add(value.display())
              }
            }
          }
        None => missing += 1
      }
    }
    if present == 0 {
      kind = KindNull
    }
    fields.push({
      name,
      kind,
      present,
      missing,
      distinct: if count_distinct {
        seen.length()
      } else {
        0
      },
    })
  }
  fields
}

///|
/// Looks up one column summary by name.
pub fn find_field(fields : Array[FieldInfo], name : String) -> FieldInfo? {
  for info in fields {
    if info.name == name {
      return Some(info)
    }
  }
  None
}