///|
/// Clean layout-disrupting control characters and expand tabs in records.
pub struct TextCleanup {
  priv cleaning : Bool
  priv tab : Int?
}

///|
pub fn TextCleanup::new() -> TextCleanup {
  { cleaning: false, tab: None, }
}

///|
pub fn TextCleanup::default() -> TextCleanup {
  TextCleanup::new()
}

///|
/// Remove C0 controls (U+0000..U+001F), preserving newlines.
/// Tabs are removed unless `expand_tabs` supplies their replacement width.
pub fn TextCleanup::remove_control_chars(self : TextCleanup) -> TextCleanup {
  { ..self, cleaning: true, }
}

///|
/// Replace each tab with `tab_width` spaces, independently of its column.
pub fn TextCleanup::expand_tabs(
  self : TextCleanup,
  tab_width : Int,
) -> TextCleanup {
  { ..self, tab: Some(Int::max(0, tab_width)), }
}

///|
/// Remove C0 controls except newlines and expand tabs to four spaces.
///
/// ```mbt check
/// test {
///   assert_eq(@tabular.TextCleanup::clean_text("a\tb\r\nc"), "a    b\nc")
/// }
/// ```
pub fn TextCleanup::clean_text(text : String) -> String {
  charset_transform(text, true, Some(4))
}

///|
fn charset_transform(text : String, clean : Bool, tab_size : Int?) -> String {
  if !clean && tab_size is None {
    return text
  }
  let out = StringBuilder()
  for ch in text {
    if ch == '\t' {
      match tab_size {
        Some(size) => out.write_string(" ".repeat(size))
        None => if !clean { out.write_char(ch) }
      }
    } else if !clean || ch == '\n' || ch >= ' ' {
      out.write_char(ch)
    }
  }
  out.to_string()
}

///|
pub impl CellOption for TextCleanup with fn change(self, table, entity) {
  if !self.cleaning && self.tab is None {
    return
  }
  for position in entity.iter(table.count_rows(), table.count_cols()) {
    let row = table.rows[position.0]
    if position.1 < row.length() {
      row[position.1] = charset_transform(
        row[position.1],
        self.cleaning,
        self.tab,
      )
    }
  }
}

///|
pub impl TableOption for TextCleanup with fn change(self, table) {
  CellOption::change(self, table, Entity::Global)
}

///|
pub extend TextCleanup with CellOption::{
  change as change_cell,
  hint_change as hint_change_cell,
}

///|
pub extend TextCleanup with TableOption::{change, hint_change}