///|
pub(all) struct TableTransformError {
  code : String
  message : String
} derive(Eq)

///|
fn table_transform_error(
  code : String,
  message : String,
) -> TableTransformError {
  { code, message }
}

///|
fn clone_header(header : Array[String]) -> Array[String] {
  let copy : Array[String] = []
  let mut i = 0
  while i < header.length() {
    copy.push(header[i])
    i = i + 1
  }
  copy
}

///|
fn clone_row(row : Array[String]) -> Array[String] {
  let copy : Array[String] = []
  let mut i = 0
  while i < row.length() {
    copy.push(row[i])
    i = i + 1
  }
  copy
}

///|
fn clone_rows(rows : Array[Array[String]]) -> Array[Array[String]] {
  let copy : Array[Array[String]] = []
  let mut i = 0
  while i < rows.length() {
    copy.push(clone_row(rows[i]))
    i = i + 1
  }
  copy
}

///|
fn header_index(header : Array[String], name : String) -> Int? {
  let mut i = 0
  while i < header.length() {
    if header[i] == name {
      return Some(i)
    }
    i = i + 1
  }
  None
}

///|
fn header_has_name(header : Array[String], name : String) -> Bool {
  match header_index(header, name) {
    Some(_) => true
    None => false
  }
}

///|
fn header_equal(left : Array[String], right : Array[String]) -> Bool {
  if left.length() != right.length() {
    return false
  }
  let mut i = 0
  while i < left.length() {
    if left[i] != right[i] {
      return false
    }
    i = i + 1
  }
  true
}

///|
fn row_equal(left : Array[String], right : Array[String]) -> Bool {
  if left.length() != right.length() {
    return false
  }
  let mut i = 0
  while i < left.length() {
    if left[i] != right[i] {
      return false
    }
    i = i + 1
  }
  true
}

///|
fn names_to_indices(
  header : Array[String],
  names : Array[String],
) -> Result[Array[Int], TableTransformError] {
  let indices : Array[Int] = []
  let mut i = 0
  while i < names.length() {
    match header_index(header, names[i]) {
      Some(index) => indices.push(index)
      None => {
        let msg =
          $|column '\{names[i]}' does not exist in table
        return Err(table_transform_error("missing_column", msg))
      }
    }
    i = i + 1
  }
  Ok(indices)
}

///|
fn build_table_from_indices(table : Table, indices : Array[Int]) -> Table {
  let header : Array[String] = []
  let mut i = 0
  while i < indices.length() {
    header.push(table.header[indices[i]])
    i = i + 1
  }
  let rows : Array[Array[String]] = []
  let mut r = 0
  while r < table.rows.length() {
    let row = table.rows[r]
    let selected : Array[String] = []
    let mut c = 0
    while c < indices.length() {
      let index = indices[c]
      let value = match row.get(index) {
        Some(v) => v
        None => ""
      }
      selected.push(value)
      c = c + 1
    }
    rows.push(selected)
    r = r + 1
  }
  { header, rows }
}

///|
fn row_has_blank(row : Array[String]) -> Bool {
  let mut i = 0
  while i < row.length() {
    if !row[i].trim().is_empty() {
      return false
    }
    i = i + 1
  }
  true
}

///|
fn find_duplicate_row(
  rows : Array[Array[String]],
  candidate : Array[String],
) -> Bool {
  let mut i = 0
  while i < rows.length() {
    if row_equal(rows[i], candidate) {
      return true
    }
    i = i + 1
  }
  false
}

///|
fn join_right_header(
  left_header : Array[String],
  right_header : Array[String],
  right_key : String,
) -> Array[String] {
  let header : Array[String] = clone_header(left_header)
  let mut i = 0
  while i < right_header.length() {
    let name = right_header[i]
    if name != right_key {
      if header_has_name(header, name) {
        let right_name =
          $|right.\{name}
        header.push(right_name)
      } else {
        header.push(name)
      }
    }
    i = i + 1
  }
  header
}

///|
fn join_row(
  left_row : Array[String],
  right_row : Array[String],
  right_header : Array[String],
  right_key : String,
) -> Array[String] {
  let row : Array[String] = clone_row(left_row)
  let mut i = 0
  while i < right_header.length() {
    let name = right_header[i]
    if name != right_key {
      let value = match right_row.get(i) {
        Some(v) => v
        None => ""
      }
      row.push(value)
    }
    i = i + 1
  }
  row
}

///|
pub fn Table::head(self : Table, n : Int) -> Table {
  let limit = if n < 0 {
    0
  } else if n > self.rows.length() {
    self.rows.length()
  } else {
    n
  }
  let rows : Array[Array[String]] = []
  let mut i = 0
  while i < limit {
    rows.push(clone_row(self.rows[i]))
    i = i + 1
  }
  { header: clone_header(self.header), rows }
}

///|
pub fn Table::tail(self : Table, n : Int) -> Table {
  let size = self.rows.length()
  let start = if n <= 0 { size } else if n >= size { 0 } else { size - n }
  let rows : Array[Array[String]] = []
  let mut i = start
  while i < size {
    rows.push(clone_row(self.rows[i]))
    i = i + 1
  }
  { header: clone_header(self.header), rows }
}

///|
pub fn Table::slice_rows(self : Table, start : Int, end : Int) -> Table {
  let size = self.rows.length()
  let from = if start < 0 { 0 } else if start > size { size } else { start }
  let to = if end < from { from } else if end > size { size } else { end }
  let rows : Array[Array[String]] = []
  let mut i = from
  while i < to {
    rows.push(clone_row(self.rows[i]))
    i = i + 1
  }
  { header: clone_header(self.header), rows }
}

///|
pub fn Table::select_columns(
  self : Table,
  names : Array[String],
) -> Result[Table, TableTransformError] {
  match names_to_indices(self.header, names) {
    Ok(indices) => Ok(build_table_from_indices(self, indices))
    Err(err) => Err(err)
  }
}

///|
pub fn Table::drop_columns(self : Table, names : Array[String]) -> Table {
  let indices : Array[Int] = []
  let mut i = 0
  while i < self.header.length() {
    if !header_has_name(names, self.header[i]) {
      indices.push(i)
    }
    i = i + 1
  }
  build_table_from_indices(self, indices)
}

///|
pub fn Table::rename_column(
  self : Table,
  from : String,
  to : String,
) -> Result[Table, TableTransformError] {
  match header_index(self.header, from) {
    Some(index) => {
      if from != to && header_has_name(self.header, to) {
        let msg =
          $|target column '\{to}' already exists
        return Err(table_transform_error("duplicate_header", msg))
      }
      let header = clone_header(self.header)
      header[index] = to
      Ok({ header, rows: clone_rows(self.rows) })
    }
    None => {
      let msg =
        $|column '\{from}' not found
      Err(table_transform_error("missing_column", msg))
    }
  }
}

///|
pub fn Table::rename_columns(
  self : Table,
  renames : Array[(String, String)],
) -> Result[Table, TableTransformError] {
  let header = clone_header(self.header)
  let mut i = 0
  while i < renames.length() {
    let (from, to) = renames[i]
    match header_index(header, from) {
      Some(index) => {
        if from != to && header_has_name(header, to) {
          let msg =
            $|target column '\{to}' already exists
          return Err(table_transform_error("duplicate_header", msg))
        }
        header[index] = to
      }
      None => {
        let msg =
          $|column '\{from}' not found
        return Err(table_transform_error("missing_column", msg))
      }
    }
    i = i + 1
  }
  Ok({ header, rows: clone_rows(self.rows) })
}

///|
pub fn Table::append_column(
  self : Table,
  name : String,
  default_value : String,
) -> Result[Table, TableTransformError] {
  if header_has_name(self.header, name) {
    let msg =
      $|column '\{name}' already exists
    return Err(table_transform_error("duplicate_header", msg))
  }
  let header = clone_header(self.header)
  header.push(name)
  let rows : Array[Array[String]] = []
  let mut i = 0
  while i < self.rows.length() {
    let row = clone_row(self.rows[i])
    row.push(default_value)
    rows.push(row)
    i = i + 1
  }
  Ok({ header, rows })
}

///|
pub fn Table::replace_column_values(
  self : Table,
  name : String,
  values : Array[String],
) -> Result[Table, TableTransformError] {
  match header_index(self.header, name) {
    Some(index) => {
      if values.length() != self.rows.length() {
        let msg =
          $|column '\{name}' expects \{self.rows.length()} values, got \{values.length()}
        return Err(table_transform_error("length_mismatch", msg))
      }
      let rows : Array[Array[String]] = []
      let mut i = 0
      while i < self.rows.length() {
        let row = clone_row(self.rows[i])
        row[index] = values[i]
        rows.push(row)
        i = i + 1
      }
      Ok({ header: clone_header(self.header), rows })
    }
    None => {
      let msg =
        $|column '\{name}' not found
      Err(table_transform_error("missing_column", msg))
    }
  }
}

///|
pub fn Table::fill_empty_cells(self : Table, replacement : String) -> Table {
  let rows : Array[Array[String]] = []
  let mut r = 0
  while r < self.rows.length() {
    let row = clone_row(self.rows[r])
    let mut c = 0
    while c < row.length() {
      if row[c].trim().is_empty() {
        row[c] = replacement
      }
      c = c + 1
    }
    rows.push(row)
    r = r + 1
  }
  { header: clone_header(self.header), rows }
}

///|
pub fn Table::fill_missing_in_column(
  self : Table,
  name : String,
  replacement : String,
) -> Result[Table, TableTransformError] {
  match header_index(self.header, name) {
    Some(index) => {
      let rows : Array[Array[String]] = []
      let mut r = 0
      while r < self.rows.length() {
        let row = clone_row(self.rows[r])
        if row.get(index) is None {
          while row.length() <= index {
            row.push("")
          }
          row[index] = replacement
        } else if row[index].trim().is_empty() {
          row[index] = replacement
        }
        rows.push(row)
        r = r + 1
      }
      Ok({ header: clone_header(self.header), rows })
    }
    None => {
      let msg =
        $|column '\{name}' not found
      Err(table_transform_error("missing_column", msg))
    }
  }
}

///|
pub fn Table::trim_cells(self : Table) -> Table {
  let rows : Array[Array[String]] = []
  let mut r = 0
  while r < self.rows.length() {
    let row = clone_row(self.rows[r])
    let mut c = 0
    while c < row.length() {
      row[c] = row[c].trim().to_owned()
      c = c + 1
    }
    rows.push(row)
    r = r + 1
  }
  { header: clone_header(self.header), rows }
}

///|
pub fn Table::remove_blank_rows(self : Table) -> Table {
  let rows : Array[Array[String]] = []
  let mut r = 0
  while r < self.rows.length() {
    let row = self.rows[r]
    if !row_has_blank(row) {
      rows.push(clone_row(row))
    }
    r = r + 1
  }
  { header: clone_header(self.header), rows }
}

///|
pub fn Table::dedupe_rows(self : Table) -> Table {
  let rows : Array[Array[String]] = []
  let mut r = 0
  while r < self.rows.length() {
    let row = self.rows[r]
    if !find_duplicate_row(rows, row) {
      rows.push(clone_row(row))
    }
    r = r + 1
  }
  { header: clone_header(self.header), rows }
}

///|
pub fn Table::concat(
  self : Table,
  other : Table,
) -> Result[Table, TableTransformError] {
  if !header_equal(self.header, other.header) {
    return Err(
      table_transform_error(
        "header_mismatch", "tables can only be concatenated when their headers match exactly",
      ),
    )
  }
  let rows = clone_rows(self.rows)
  let mut i = 0
  while i < other.rows.length() {
    rows.push(clone_row(other.rows[i]))
    i = i + 1
  }
  Ok({ header: clone_header(self.header), rows })
}

///|
pub fn Table::column_values(
  self : Table,
  name : String,
) -> Result[Array[String], TableTransformError] {
  match header_index(self.header, name) {
    Some(index) => {
      let values : Array[String] = []
      let mut i = 0
      while i < self.rows.length() {
        let value = match self.rows[i].get(index) {
          Some(v) => v
          None => ""
        }
        values.push(value)
        i = i + 1
      }
      Ok(values)
    }
    None => {
      let msg =
        $|column '\{name}' not found
      Err(table_transform_error("missing_column", msg))
    }
  }
}

///|
pub fn Table::value_counts(
  self : Table,
  name : String,
) -> Result[Array[(String, Int)], TableTransformError] {
  match self.column_values(name) {
    Ok(values) => {
      let counts : Array[(String, Int)] = []
      let mut i = 0
      while i < values.length() {
        let value = values[i]
        let mut found = false
        let mut j = 0
        while j < counts.length() {
          let (candidate, count) = counts[j]
          if candidate == value {
            counts[j] = (candidate, count + 1)
            found = true
            break
          }
          j = j + 1
        }
        if !found {
          counts.push((value, 1))
        }
        i = i + 1
      }
      Ok(counts)
    }
    Err(err) => Err(err)
  }
}

///|
pub fn Table::inner_join(
  self : Table,
  other : Table,
  left_key : String,
  right_key : String,
) -> Result[Table, TableTransformError] {
  match header_index(self.header, left_key) {
    Some(left_index) =>
      match header_index(other.header, right_key) {
        Some(right_index) => {
          let header = join_right_header(self.header, other.header, right_key)
          let rows : Array[Array[String]] = []
          let mut left_row_index = 0
          while left_row_index < self.rows.length() {
            let left_row = self.rows[left_row_index]
            let left_value = match left_row.get(left_index) {
              Some(v) => v
              None => ""
            }
            let mut right_row_index = 0
            while right_row_index < other.rows.length() {
              let right_row = other.rows[right_row_index]
              let right_value = match right_row.get(right_index) {
                Some(v) => v
                None => ""
              }
              if left_value == right_value {
                rows.push(
                  join_row(left_row, right_row, other.header, right_key),
                )
              }
              right_row_index = right_row_index + 1
            }
            left_row_index = left_row_index + 1
          }
          Ok({ header, rows })
        }
        None => {
          let msg =
            $|column '\{right_key}' not found in right table
          Err(table_transform_error("missing_column", msg))
        }
      }
    None => {
      let msg =
        $|column '\{left_key}' not found in left table
      Err(table_transform_error("missing_column", msg))
    }
  }
}