///|
/// A scalar feature value before it is encoded into a numeric vector.
pub enum FeatureValue {
  Number(Double)
  Category(String)
  Boolean(Bool)
  Missing
} derive(ToJson, FromJson, Debug, Eq)

///|
pub fn FeatureValue::as_number(
  self : FeatureValue,
  missing_value? : Double = 0.0,
) -> Double {
  match self {
    Number(value) => value
    Boolean(value) => if value { 1.0 } else { 0.0 }
    Category(_) => missing_value
    Missing => missing_value
  }
}

///|
pub fn FeatureValue::is_missing(self : FeatureValue) -> Bool {
  match self {
    Missing => true
    _ => false
  }
}

///|
pub struct CsvOptions {
  separator : Char
  quote : Char
  escape : Char
  trim_fields : Bool
  skip_empty : Bool
}

///|
pub fn CsvOptions::new(
  separator? : Char = ',',
  quote? : Char = '"',
  escape? : Char = '"',
  trim_fields? : Bool = true,
  skip_empty? : Bool = false,
) -> CsvOptions {
  { separator, quote, escape, trim_fields, skip_empty }
}

///|
pub fn CsvOptions::separator(self : CsvOptions) -> Char {
  self.separator
}

///|
pub fn CsvOptions::parse_line(
  self : CsvOptions,
  line : String,
) -> Array[String] {
  let fields = Array::make(0, "")
  let current = Ref("")
  let quoted = Ref(false)
  for character in line {
    if character == self.quote {
      quoted.val = !quoted.val
    } else if character == self.separator && !quoted.val {
      let field = if self.trim_fields {
        current.val.trim().to_owned()
      } else {
        current.val
      }
      if !self.skip_empty || field != "" {
        fields.push(field)
      }
      current.val = ""
    } else {
      current.val = String::add(current.val, "\{character}")
    }
  }
  let field = if self.trim_fields {
    current.val.trim().to_owned()
  } else {
    current.val
  }
  if !self.skip_empty || field != "" {
    fields.push(field)
  }
  fields
}

///|
pub fn CsvOptions::parse_lines(
  self : CsvOptions,
  text : String,
) -> Array[Array[String]] {
  let rows = Array::make(0, [])
  for line in text.split("\n") {
    let value = line.to_owned()
    if value != "" || !self.skip_empty {
      rows.push(self.parse_line(value))
    }
  }
  rows
}

///|
pub fn csv_escape(
  field : String,
  options? : CsvOptions = CsvOptions::new(),
) -> String {
  let needs_quote = field.contains_char(options.separator) ||
    field.contains_char(options.quote) ||
    field.contains_char('\n')
  if !needs_quote {
    field
  } else {
    let escaped = field.replace(
      old="\{options.quote}",
      new="\{options.quote}\{options.quote}",
    )
    "\{options.quote}\{escaped}\{options.quote}"
  }
}

///|
pub fn csv_write_row(
  fields : Array[String],
  options? : CsvOptions = CsvOptions::new(),
) -> String {
  fields
  .map(field => csv_escape(field, options~))
  .join(options.separator.to_string())
}

///|
pub struct CategoricalEncoder {
  values : Map[String, Int]
  mut next_index : Int
  unknown_index : Int?
}

///|
pub fn CategoricalEncoder::new(unknown_index? : Int) -> CategoricalEncoder {
  { values: {}, next_index: 0, unknown_index }
}

///|
pub fn CategoricalEncoder::encode(
  self : CategoricalEncoder,
  value : String,
) -> Int {
  match self.values.get(value) {
    Some(index) => index
    None =>
      match self.unknown_index {
        Some(index) => index
        None => {
          let index = self.next_index
          self.values[value] = index
          self.next_index += 1
          index
        }
      }
  }
}

///|
pub fn CategoricalEncoder::contains(
  self : CategoricalEncoder,
  value : String,
) -> Bool {
  self.values.contains(value)
}

///|
pub fn CategoricalEncoder::dimension(self : CategoricalEncoder) -> Int {
  self.next_index
}

///|
pub fn CategoricalEncoder::entries(
  self : CategoricalEncoder,
) -> Array[(String, Int)] {
  self.values.to_array()
}

///|
pub fn CategoricalEncoder::reset(self : CategoricalEncoder) -> Unit {
  self.values.clear()
  self.next_index = 0
}

///|
pub struct FeatureHasher {
  buckets : Int
  signed : Bool
}

///|
pub fn FeatureHasher::new(
  buckets : Int,
  signed? : Bool = true,
) -> FeatureHasher {
  { buckets: if buckets < 0 { 0 } else { buckets }, signed }
}

///|
pub fn FeatureHasher::buckets(self : FeatureHasher) -> Int {
  self.buckets
}

///|
pub fn FeatureHasher::index(self : FeatureHasher, token : String) -> Int {
  hash_feature(token, self.buckets)
}

///|
pub fn FeatureHasher::encode(
  self : FeatureHasher,
  tokens : Array[String],
) -> SparseVector {
  let entries = Array::make(0, SparseEntry::new(0, 0.0))
  for token in tokens {
    let entry = if self.signed {
      hash_feature_with_sign(token, self.buckets)
    } else {
      SparseEntry::new(self.index(token), 1.0)
    }
    entries.push(entry)
  }
  SparseVector::from_entries(self.buckets, entries)
}

///|
pub fn FeatureHasher::encode_weighted(
  self : FeatureHasher,
  tokens : Array[(String, Double)],
) -> SparseVector {
  let entries = Array::make(0, SparseEntry::new(0, 0.0))
  for item in tokens {
    let token = item.0
    let weight = item.1
    let index = self.index(token)
    let sign = if self.signed && token.hash() % 2 != 0 { -1.0 } else { 1.0 }
    entries.push(SparseEntry::new(index, sign * weight))
  }
  SparseVector::from_entries(self.buckets, entries)
}

///|
pub fn FeatureHasher::encode_map(
  self : FeatureHasher,
  values : Map[String, Double],
) -> SparseVector {
  let entries = Array::make(0, SparseEntry::new(0, 0.0))
  values.each((token, weight) => {
    let index = self.index(token)
    let sign = if self.signed && token.hash() % 2 != 0 { -1.0 } else { 1.0 }
    entries.push(SparseEntry::new(index, sign * weight))
  })
  SparseVector::from_entries(self.buckets, entries)
}

///|
pub fn cross_feature(
  left : String,
  right : String,
  separator? : String = "=",
) -> String {
  "\{left}\{separator}\{right}"
}

///|
pub fn generate_feature_crosses(
  features : Array[String],
  order? : Int = 2,
) -> Array[String] {
  let result = Array::make(0, "")
  let safe_order = if order < 1 { 1 } else { order }
  if safe_order == 1 {
    result.append(features[:])
  } else {
    for i in 0.. DataBatch {
  let _ = dimension
  { features: [], labels: [], weights: [] }
}

///|
pub fn DataBatch::from_arrays(
  features : Array[Array[Double]],
  labels : Array[Double],
) -> DataBatch {
  let weights = Array::make(labels.length(), 1.0)
  { features, labels, weights }
}

///|
pub fn DataBatch::add(
  self : DataBatch,
  features : Array[Double],
  label : Double,
  weight? : Double = 1.0,
) -> Bool {
  if !self.features.is_empty() && features.length() != self.features[0].length() {
    false
  } else {
    self.features.push(copy_vector(features))
    self.labels.push(label)
    self.weights.push(weight)
    true
  }
}

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

///|
pub fn DataBatch::dimension(self : DataBatch) -> Int {
  if self.features.is_empty() {
    0
  } else {
    self.features[0].length()
  }
}

///|
pub fn DataBatch::features(self : DataBatch) -> Array[Array[Double]] {
  self.features.map(row => copy_vector(row))
}

///|
pub fn DataBatch::labels(self : DataBatch) -> Array[Double] {
  copy_vector(self.labels)
}

///|
pub fn DataBatch::weights(self : DataBatch) -> Array[Double] {
  copy_vector(self.weights)
}

///|
pub fn DataBatch::validate(self : DataBatch) -> ValidationReport {
  if self.features.length() != self.labels.length() ||
    self.labels.length() != self.weights.length() {
    ValidationReport::error("batch arrays have different lengths")
  } else if self.features.is_empty() {
    ValidationReport::ok()
  } else {
    let dimension = self.features[0].length()
    let mut valid = true
    for row in self.features {
      if row.length() != dimension {
        valid = false
      }
    }
    if valid {
      ValidationReport::ok()
    } else {
      ValidationReport::error("batch contains inconsistent feature dimensions")
    }
  }
}

///|
pub fn DataBatch::shuffle(
  self : DataBatch,
  random_index : (Int) -> Int,
) -> Unit {
  let size = self.size()
  if size > 1 {
    for i in 0..= 0 && j < size {
        self.features.swap(i, j)
        self.labels.swap(i, j)
        self.weights.swap(i, j)
      }
    }
  }
}

///|
pub fn DataBatch::slice(self : DataBatch, start : Int, end : Int) -> DataBatch {
  let from = if start < 0 {
    0
  } else if start > self.size() {
    self.size()
  } else {
    start
  }
  let to = if end < from {
    from
  } else if end > self.size() {
    self.size()
  } else {
    end
  }
  let features = Array::makei(to - from, i => {
    copy_vector(self.features[from + i])
  })
  let labels = Array::makei(to - from, i => self.labels[from + i])
  let weights = Array::makei(to - from, i => self.weights[from + i])
  { features, labels, weights }
}

///|
pub struct StreamCounters {
  mut rows : Int
  mut accepted : Int
  mut rejected : Int
  mut positive : Int
  mut negative : Int
}

///|
pub fn StreamCounters::new() -> StreamCounters {
  { rows: 0, accepted: 0, rejected: 0, positive: 0, negative: 0 }
}

///|
pub fn StreamCounters::observe(
  self : StreamCounters,
  accepted : Bool,
  label? : Double = 0.0,
) -> Unit {
  self.rows += 1
  if accepted {
    self.accepted += 1
    if label >= 0.5 {
      self.positive += 1
    } else {
      self.negative += 1
    }
  } else {
    self.rejected += 1
  }
}

///|
pub fn StreamCounters::rows(self : StreamCounters) -> Int {
  self.rows
}

///|
pub fn StreamCounters::accepted(self : StreamCounters) -> Int {
  self.accepted
}

///|
pub fn StreamCounters::rejected(self : StreamCounters) -> Int {
  self.rejected
}

///|
pub fn StreamCounters::acceptance_rate(self : StreamCounters) -> Double {
  if self.rows == 0 {
    0.0
  } else {
    self.accepted.to_double() / self.rows.to_double()
  }
}