// Records, tables and the diagnostics that parsing produces.

///|
/// Something that went wrong while reading input. Diagnostics never stop a
/// parse: the offending line is skipped and reported, so one broken line in a
/// million still leaves a usable table.
pub(all) struct Diagnostic {
  code : String
  line : Int
  message : String
} derive(Eq, Debug)

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

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

///|
pub fn Diagnostic::new(
  code : String,
  line : Int,
  message : String,
) -> Diagnostic {
  { code, line, message, }
}

///|
/// One row. `fields` holds the extracted values, `order` remembers the order in
/// which field names were first seen so that output columns stay stable, and
/// `raw` keeps the source text for `parse` stages and for debugging.
pub(all) struct Record {
  fields : Map[String, Value]
  order : Array[String]
  source_line : Int
  raw : String
} derive(Debug)

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

///|
pub fn Record::empty(source_line : Int, raw : String) -> Record {
  { fields: Map([]), order: [], source_line, raw, }
}

///|
/// The value of a field, or `Null` when the record does not carry it.
pub fn Record::field(self : Record, name : String) -> Value {
  match self.fields.get(name) {
    Some(v) => v
    None => Null
  }
}

///|
pub fn Record::has(self : Record, name : String) -> Bool {
  self.fields.contains(name)
}

///|
/// Stores a value. A new name is appended to the column order, an existing one
/// keeps its position.
pub fn Record::set(self : Record, name : String, value : Value) -> Unit {
  if !self.fields.contains(name) {
    self.order.push(name)
  }
  self.fields[name] = value
}

///|
/// Stores a value only when the name is not present yet, which is how a parser
/// keeps the first occurrence of a repeated key.
pub fn Record::set_if_absent(
  self : Record,
  name : String,
  value : Value,
) -> Unit {
  if !self.fields.contains(name) {
    self.set(name, value)
  }
}

///|
pub fn Record::remove(self : Record, name : String) -> Unit {
  if self.fields.contains(name) {
    self.fields.remove(name)
    let kept : Array[String] = []
    for name_in_order in self.order {
      if name_in_order != name {
        kept.push(name_in_order)
      }
    }
    self.order.clear()
    self.order.push_iter(kept.iter())
  }
}

///|
/// Field names in first-seen order.
pub fn Record::names(self : Record) -> Array[String] {
  self.order.copy()
}

///|
pub fn Record::size(self : Record) -> Int {
  self.order.length()
}

///|
/// A table: an ordered set of columns plus the rows that carry them.
pub(all) struct Table {
  columns : Array[String]
  records : Array[Record]
  diagnostics : Array[Diagnostic]
} derive(Debug)

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

///|
pub fn Table::empty() -> Table {
  { columns: [], records: [], diagnostics: [], }
}

///|
pub fn Table::of_records(
  columns : Array[String],
  records : Array[Record],
) -> Table {
  let table : Table = { columns: [], records, diagnostics: [], }
  for name in columns {
    table.add_column(name)
  }
  for record in records {
    for name in record.order {
      table.add_column(name)
    }
  }
  table
}

///|
/// Registers a column name, keeping first-seen order and ignoring repeats.
pub fn Table::add_column(self : Table, name : String) -> Unit {
  if !self.columns.contains(name) {
    self.columns.push(name)
  }
}

///|
pub fn Table::push(self : Table, record : Record) -> Unit {
  for name in record.order {
    self.add_column(name)
  }
  self.records.push(record)
}

///|
pub fn Table::add_diagnostic(self : Table, diagnostic : Diagnostic) -> Unit {
  self.diagnostics.push(diagnostic)
}

///|
pub fn Table::length(self : Table) -> Int {
  self.records.length()
}

///|
pub fn Table::is_empty(self : Table) -> Bool {
  self.records.is_empty()
}

///|
/// Column names in stable order, widened by any field that only some rows carry.
pub fn Table::column_names(self : Table) -> Array[String] {
  self.columns.copy()
}

///|
/// A parsed input together with how it was read.
pub(all) struct Dataset {
  table : Table
  format : String
  mut lines_read : Int
  mut skipped : Int
} derive(Debug)

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

///|
pub fn Dataset::empty(format : String) -> Dataset {
  { table: Table::empty(), format, lines_read: 0, skipped: 0, }
}

///|
pub fn Dataset::add(self : Dataset, record : Record) -> Unit {
  self.table.push(record)
}

///|
pub fn Dataset::skip(self : Dataset, diagnostic : Diagnostic) -> Unit {
  self.table.add_diagnostic(diagnostic)
  self.skipped += 1
}

///|
pub fn Dataset::rows(self : Dataset) -> Int {
  self.table.length()
}