///|
pub let default_row_count : Int = 30

///|
pub let default_col_count : Int = 12

///|
pub(all) struct Selection {
  row : Int
  col : Int
} derive(Eq, Debug)

///|
pub(all) struct Clipboard {
  cells : Array[Array[@cell.Cell]]
  origin : Selection
} derive(Eq, Debug)

///|
pub(all) struct IndexedCell {
  index : Int
  cell : @cell.Cell
} derive(Eq, Debug)

///|
pub(all) struct CellChange {
  row : Int
  col : Int
  old_cell : @cell.Cell
  new_cell : @cell.Cell
} derive(Eq, Debug)

///|
priv struct SortRow {
  source_row : Int
  cells : Array[@cell.Cell]
}

///|
priv struct FormulaRefToken {
  addr : @cell.CellAddress
  col_abs : Bool
  row_abs : Bool
}

///|
pub(all) enum UndoOp {
  CellEdit(Int, Int, @cell.Cell, @cell.Cell)
  CellBatchEdit(Array[CellChange])
  CellFormat(Int, Int, @cell.CellFormat, @cell.CellFormat)
  RowInsert(Int)
  ColumnInsert(Int)
  RowDelete(Int, Array[IndexedCell])
  ColumnDelete(Int, Array[IndexedCell])
} derive(Eq, Debug)

///|
pub(all) struct Sheet {
  name : String
  row_count : Int
  col_count : Int
  cells : Map[String, @cell.Cell]
} derive(Debug)

///|
pub(all) struct Workbook {
  sheets : Array[Sheet]
  active_sheet : Int
  selection : Selection?
  selection_range : @cell.CellRange?
  clipboard : Clipboard
  undo_stack : Array[UndoOp]
  redo_stack : Array[UndoOp]
} derive(Debug)

///|
pub fn Selection::new(row~ : Int, col~ : Int) -> Selection {
  { row, col }
}

///|
pub fn Selection::key(self : Selection) -> String {
  @cell.make_key(self.row, self.col)
}

///|
pub fn Clipboard::empty() -> Clipboard {
  { cells: [], origin: Selection::new(row=0, col=0) }
}

///|
pub fn Sheet::new(
  name : String,
  row_count? : Int = default_row_count,
  col_count? : Int = default_col_count,
) -> Sheet {
  { name, row_count, col_count, cells: Map([]) }
}

///|
pub fn Sheet::from_table(
  name : String,
  headers : Array[String],
  rows : Array[Array[String]],
) -> Sheet {
  let sheet = Sheet::new(
    name,
    row_count=rows.length() + 1,
    col_count=headers.length(),
  )
  let mut result = sheet
  for col, value in headers {
    result = result.set_raw_no_undo(0, col, value)
  }
  for row, values in rows {
    for col, value in values {
      result = result.set_raw_no_undo(row + 1, col, value)
    }
  }
  result.recalc()
}

///|
pub fn Sheet::get_cell(self : Sheet, row : Int, col : Int) -> @cell.Cell {
  match self.cells.get(@cell.make_key(row, col)) {
    Some(cell) => cell
    None => @cell.Cell::empty()
  }
}

///|
pub fn Sheet::display_cell(self : Sheet, row : Int, col : Int) -> String {
  self.get_cell(row, col).display()
}

///|
pub fn Sheet::safe_raw(self : Sheet, row : Int, col : Int) -> String {
  self.get_cell(row, col).raw
}

///|
pub fn Sheet::set_raw_no_undo(
  self : Sheet,
  row : Int,
  col : Int,
  raw : String,
) -> Sheet {
  let cells = self.cells.copy()
  let old = self.get_cell(row, col)
  let next_cell = { ..old, raw, value: @cell.parse_raw_value(raw) }
  let key = @cell.make_key(row, col)
  if next_cell.is_empty() {
    cells.remove(key)
  } else {
    cells[key] = next_cell
  }
  {
    ..self,
    row_count: max_int(self.row_count, row + 1),
    col_count: max_int(self.col_count, col + 1),
    cells,
  }
}

///|
pub fn Sheet::set_cell_no_undo(
  self : Sheet,
  row : Int,
  col : Int,
  next_cell : @cell.Cell,
) -> Sheet {
  let cells = self.cells.copy()
  let key = @cell.make_key(row, col)
  if next_cell.is_empty() {
    cells.remove(key)
  } else {
    cells[key] = next_cell
  }
  {
    ..self,
    row_count: max_int(self.row_count, row + 1),
    col_count: max_int(self.col_count, col + 1),
    cells,
  }
}

///|
pub fn Sheet::set_format_no_undo(
  self : Sheet,
  row : Int,
  col : Int,
  format : @cell.CellFormat,
) -> Sheet {
  let old = self.get_cell(row, col)
  self.set_cell_no_undo(row, col, { ..old, format, }).recalc()
}

///|
pub fn Sheet::recalc(self : Sheet) -> Sheet {
  let mut current = self
  for _ in 0..<3 {
    let base = current
    let cells = current.cells.copy()
    for key, existing in base.cells {
      let value = @formula.evaluate(existing.raw, ref_str => {
        match @cell.parse_ref(ref_str) {
          Some(addr) => base.get_cell(addr.row, addr.col).value
          None => @cell.CellValue::Empty
        }
      })
      cells[key] = { ..existing, value, }
    }
    current = { ..current, cells, }
  }
  current
}

///|
pub fn Sheet::used_cell_count(self : Sheet) -> Int {
  let mut count = 0
  for _, cell in self.cells {
    if !cell.raw.is_empty() {
      count = count + 1
    }
  }
  count
}

///|
pub fn Workbook::new() -> Workbook {
  Workbook::from_sheets([Sheet::new("Sheet1")])
}

///|
pub fn Workbook::from_sheets(
  sheets : Array[Sheet],
  active_sheet? : Int = 0,
) -> Workbook {
  let source = if sheets.is_empty() { [Sheet::new("Sheet1")] } else { sheets }
  let active = clamp_int(active_sheet, 0, source.length() - 1)
  {
    sheets: source,
    active_sheet: active,
    selection: None,
    selection_range: None,
    clipboard: Clipboard::empty(),
    undo_stack: [],
    redo_stack: [],
  }
}

///|
pub fn Workbook::demo() -> Workbook {
  let sales = Sheet::new("Sales")
    .set_raw_no_undo(0, 0, "Product")
    .set_raw_no_undo(0, 1, "Q1")
    .set_raw_no_undo(0, 2, "Q2")
    .set_raw_no_undo(0, 3, "Q3")
    .set_raw_no_undo(0, 4, "Q4")
    .set_raw_no_undo(0, 5, "Total")
    .set_raw_no_undo(1, 0, "Widget A")
    .set_raw_no_undo(1, 1, "1200")
    .set_raw_no_undo(1, 2, "1350")
    .set_raw_no_undo(1, 3, "1420")
    .set_raw_no_undo(1, 4, "1580")
    .set_raw_no_undo(1, 5, "=SUM(B2:E2)")
    .set_raw_no_undo(2, 0, "Widget B")
    .set_raw_no_undo(2, 1, "800")
    .set_raw_no_undo(2, 2, "920")
    .set_raw_no_undo(2, 3, "1100")
    .set_raw_no_undo(2, 4, "980")
    .set_raw_no_undo(2, 5, "=SUM(B3:E3)")
    .set_raw_no_undo(3, 0, "Gadget X")
    .set_raw_no_undo(3, 1, "2500")
    .set_raw_no_undo(3, 2, "2800")
    .set_raw_no_undo(3, 3, "3100")
    .set_raw_no_undo(3, 4, "2900")
    .set_raw_no_undo(3, 5, "=SUM(B4:E4)")
    .set_raw_no_undo(4, 0, "Gadget Y")
    .set_raw_no_undo(4, 1, "600")
    .set_raw_no_undo(4, 2, "750")
    .set_raw_no_undo(4, 3, "810")
    .set_raw_no_undo(4, 4, "920")
    .set_raw_no_undo(4, 5, "=SUM(B5:E5)")
    .set_raw_no_undo(5, 0, "Module Z")
    .set_raw_no_undo(5, 1, "1800")
    .set_raw_no_undo(5, 2, "1650")
    .set_raw_no_undo(5, 3, "1900")
    .set_raw_no_undo(5, 4, "2100")
    .set_raw_no_undo(5, 5, "=SUM(B6:E6)")
    .recalc()
  let inventory = Sheet::new("Inventory")
    .set_raw_no_undo(0, 0, "Product")
    .set_raw_no_undo(0, 1, "Stock")
    .set_raw_no_undo(0, 2, "Warehouse")
    .set_raw_no_undo(0, 3, "Status")
    .set_raw_no_undo(1, 0, "Widget A")
    .set_raw_no_undo(1, 1, "500")
    .set_raw_no_undo(1, 2, "A1")
    .set_raw_no_undo(1, 3, "OK")
    .set_raw_no_undo(2, 0, "Widget B")
    .set_raw_no_undo(2, 1, "120")
    .set_raw_no_undo(2, 2, "A2")
    .set_raw_no_undo(2, 3, "Low")
    .set_raw_no_undo(3, 0, "Gadget X")
    .set_raw_no_undo(3, 1, "800")
    .set_raw_no_undo(3, 2, "B1")
    .set_raw_no_undo(3, 3, "OK")
    .set_raw_no_undo(4, 0, "Gadget Y")
    .set_raw_no_undo(4, 1, "30")
    .set_raw_no_undo(4, 2, "B2")
    .set_raw_no_undo(4, 3, "Critical")
    .set_raw_no_undo(5, 0, "Module Z")
    .set_raw_no_undo(5, 1, "200")
    .set_raw_no_undo(5, 2, "C1")
    .set_raw_no_undo(5, 3, "OK")
    .recalc()
  Workbook::from_sheets([sales, inventory])
}

///|
pub fn Workbook::active(self : Workbook) -> Sheet {
  self.sheets[self.active_sheet]
}

///|
pub fn Workbook::sheet_names(self : Workbook) -> Array[String] {
  self.sheets.map(sheet => sheet.name)
}

///|
pub fn Workbook::selected_cell(self : Workbook) -> @cell.Cell {
  match self.selection {
    Some(sel) => self.active().get_cell(sel.row, sel.col)
    None => @cell.Cell::empty()
  }
}

///|
pub fn Workbook::selected_ref(self : Workbook) -> String {
  match self.selection_range {
    Some(range) => range_ref(range)
    None =>
      match self.selection {
        Some(sel) => sel.key()
        None => ""
      }
  }
}

///|
pub fn Workbook::select_cell(self : Workbook, row : Int, col : Int) -> Workbook {
  let sheet = self.active()
  let next = Selection::new(
    row=clamp_int(row, 0, sheet.row_count - 1),
    col=clamp_int(col, 0, sheet.col_count - 1),
  )
  { ..self, selection: Some(next), selection_range: None }
}

///|
pub fn Workbook::select_range(
  self : Workbook,
  start_row : Int,
  start_col : Int,
  end_row : Int,
  end_col : Int,
) -> Workbook {
  let sheet = self.active()
  let anchor_row = clamp_int(start_row, 0, sheet.row_count - 1)
  let anchor_col = clamp_int(start_col, 0, sheet.col_count - 1)
  let row_start = clamp_int(min_int(start_row, end_row), 0, sheet.row_count - 1)
  let row_end = clamp_int(max_int(start_row, end_row), 0, sheet.row_count - 1)
  let col_start = clamp_int(min_int(start_col, end_col), 0, sheet.col_count - 1)
  let col_end = clamp_int(max_int(start_col, end_col), 0, sheet.col_count - 1)
  {
    ..self,
    selection: Some(Selection::new(row=anchor_row, col=anchor_col)),
    selection_range: Some(
      @cell.CellRange::new(
        start=@cell.CellAddress::new(row=row_start, col=col_start),
        end=@cell.CellAddress::new(row=row_end, col=col_end),
      ),
    ),
  }
}

///|
pub fn Workbook::selected_range(self : Workbook) -> @cell.CellRange? {
  self.selection_range
}

///|
pub fn Workbook::cell_is_selected(
  self : Workbook,
  row : Int,
  col : Int,
) -> Bool {
  match self.selection_range {
    Some(range) =>
      row >= range.start.row &&
      row <= range.end.row &&
      col >= range.start.col &&
      col <= range.end.col
    None =>
      match self.selection {
        Some(sel) => sel.row == row && sel.col == col
        None => false
      }
  }
}

///|
fn range_ref(range : @cell.CellRange) -> String {
  let start = range.start.key()
  let end = range.end.key()
  if start == end {
    start
  } else {
    start + ":" + end
  }
}

///|
pub fn Workbook::switch_sheet(self : Workbook, index : Int) -> Workbook {
  if index < 0 || index >= self.sheets.length() {
    self
  } else {
    {
      ..self,
      active_sheet: index,
      selection: None,
      selection_range: None,
      undo_stack: [],
      redo_stack: [],
    }
  }
}

///|
pub fn Workbook::navigate(self : Workbook, drow : Int, dcol : Int) -> Workbook {
  match self.selection {
    None => self.select_cell(0, 0)
    Some(sel) => self.select_cell(sel.row + drow, sel.col + dcol)
  }
}

///|
pub fn Workbook::navigate_to_data_edge(
  self : Workbook,
  drow : Int,
  dcol : Int,
) -> Workbook {
  match self.selection {
    None => self.select_cell(0, 0)
    Some(sel) => {
      let edge = self.active().data_edge_position(sel.row, sel.col, drow, dcol)
      self.select_cell(edge.row, edge.col)
    }
  }
}

///|
pub fn Workbook::extend_selection(
  self : Workbook,
  drow : Int,
  dcol : Int,
) -> Workbook {
  match self.selection {
    None => self.select_cell(0, 0)
    Some(anchor) => {
      let sheet = self.active()
      let focus = match self.selection_range {
        Some(range) =>
          Selection::new(
            row=if anchor.row == range.start.row {
              range.end.row
            } else {
              range.start.row
            },
            col=if anchor.col == range.start.col {
              range.end.col
            } else {
              range.start.col
            },
          )
        None => anchor
      }
      let next_row = clamp_int(focus.row + drow, 0, sheet.row_count - 1)
      let next_col = clamp_int(focus.col + dcol, 0, sheet.col_count - 1)
      self.select_range(anchor.row, anchor.col, next_row, next_col)
    }
  }
}

///|
pub fn Workbook::extend_selection_to(
  self : Workbook,
  row : Int,
  col : Int,
) -> Workbook {
  match self.selection {
    None => self.select_cell(row, col)
    Some(anchor) => self.select_range(anchor.row, anchor.col, row, col)
  }
}

///|
pub fn Workbook::extend_selection_to_data_edge(
  self : Workbook,
  drow : Int,
  dcol : Int,
) -> Workbook {
  match self.selection {
    None => self.select_cell(0, 0)
    Some(anchor) => {
      let edge = self
        .active()
        .data_edge_position(anchor.row, anchor.col, drow, dcol)
      self.select_range(anchor.row, anchor.col, edge.row, edge.col)
    }
  }
}

///|
pub fn Workbook::set_selected_raw(self : Workbook, raw : String) -> Workbook {
  match self.selection {
    None => self
    Some(sel) => self.set_raw(sel.row, sel.col, raw)
  }
}

///|
pub fn Workbook::set_raw(
  self : Workbook,
  row : Int,
  col : Int,
  raw : String,
) -> Workbook {
  let sheet = self.active()
  let old_cell = sheet.get_cell(row, col)
  let updated_sheet = sheet.set_raw_no_undo(row, col, raw).recalc()
  let new_cell = updated_sheet.get_cell(row, col)
  let next = self.with_active_sheet(updated_sheet)
  if old_cell == new_cell {
    next
  } else {
    next.push_undo(UndoOp::CellEdit(row, col, old_cell, new_cell))
  }
}

///|
pub fn Workbook::set_selected_format(
  self : Workbook,
  updater : (@cell.CellFormat) -> @cell.CellFormat,
) -> Workbook {
  match self.selection_range {
    Some(range) =>
      self.set_format_range(
        range.start.row,
        range.start.col,
        range.end.row,
        range.end.col,
        updater,
      )
    None =>
      match self.selection {
        None => self
        Some(sel) => self.set_format(sel.row, sel.col, updater)
      }
  }
}

///|
pub fn Workbook::set_format_range(
  self : Workbook,
  start_row : Int,
  start_col : Int,
  end_row : Int,
  end_col : Int,
  updater : (@cell.CellFormat) -> @cell.CellFormat,
) -> Workbook {
  let sheet = self.active()
  let row_start = clamp_int(min_int(start_row, end_row), 0, sheet.row_count - 1)
  let row_end = clamp_int(max_int(start_row, end_row), 0, sheet.row_count - 1)
  let col_start = clamp_int(min_int(start_col, end_col), 0, sheet.col_count - 1)
  let col_end = clamp_int(max_int(start_col, end_col), 0, sheet.col_count - 1)
  let changes : Array[CellChange] = []
  let mut updated_sheet = sheet
  for row in row_start..<=row_end {
    for col in col_start..<=col_end {
      let old_cell = updated_sheet.get_cell(row, col)
      let new_format = updater(old_cell.format)
      if old_cell.format != new_format {
        let new_cell = { ..old_cell, format: new_format }
        updated_sheet = updated_sheet.set_cell_no_undo(row, col, new_cell)
        changes.push({ row, col, old_cell, new_cell })
      }
    }
  }
  let next = self.with_active_sheet(updated_sheet.recalc())
  if changes.is_empty() {
    next
  } else if changes.length() == 1 {
    let change = changes[0]
    next.push_undo(
      UndoOp::CellEdit(change.row, change.col, change.old_cell, change.new_cell),
    )
  } else {
    next.push_undo(UndoOp::CellBatchEdit(changes))
  }
}

///|
pub fn Workbook::set_format(
  self : Workbook,
  row : Int,
  col : Int,
  updater : (@cell.CellFormat) -> @cell.CellFormat,
) -> Workbook {
  let sheet = self.active()
  let old_cell = sheet.get_cell(row, col)
  let old_format = old_cell.format
  let new_format = updater(old_format)
  if old_format == new_format {
    self
  } else {
    let updated_sheet = sheet.set_format_no_undo(row, col, new_format)
    self
    .with_active_sheet(updated_sheet)
    .push_undo(UndoOp::CellFormat(row, col, old_format, new_format))
  }
}

///|
pub fn Workbook::clear_selected(self : Workbook) -> Workbook {
  match self.selection_range {
    Some(range) =>
      self.clear_range(
        range.start.row,
        range.start.col,
        range.end.row,
        range.end.col,
      )
    None =>
      match self.selection {
        None => self
        Some(sel) => self.clear_range(sel.row, sel.col, sel.row, sel.col)
      }
  }
}

///|
pub fn Workbook::clear_range(
  self : Workbook,
  start_row : Int,
  start_col : Int,
  end_row : Int,
  end_col : Int,
) -> Workbook {
  let sheet = self.active()
  let row_start = clamp_int(min_int(start_row, end_row), 0, sheet.row_count - 1)
  let row_end = clamp_int(max_int(start_row, end_row), 0, sheet.row_count - 1)
  let col_start = clamp_int(min_int(start_col, end_col), 0, sheet.col_count - 1)
  let col_end = clamp_int(max_int(start_col, end_col), 0, sheet.col_count - 1)
  let changes : Array[CellChange] = []
  let empty = @cell.Cell::empty()
  let mut updated_sheet = sheet
  for row in row_start..<=row_end {
    for col in col_start..<=col_end {
      let old_cell = updated_sheet.get_cell(row, col)
      if old_cell != empty {
        updated_sheet = updated_sheet.set_cell_no_undo(row, col, empty)
        changes.push({ row, col, old_cell, new_cell: empty })
      }
    }
  }
  let next = self.with_active_sheet(updated_sheet.recalc())
  if changes.is_empty() {
    next
  } else if changes.length() == 1 {
    let change = changes[0]
    next.push_undo(
      UndoOp::CellEdit(change.row, change.col, change.old_cell, change.new_cell),
    )
  } else {
    next.push_undo(UndoOp::CellBatchEdit(changes))
  }
}

///|
pub fn Workbook::copy_selected(self : Workbook) -> Workbook {
  match self.selection_range {
    Some(range) =>
      self.copy_range(
        range.start.row,
        range.start.col,
        range.end.row,
        range.end.col,
      )
    None =>
      match self.selection {
        None => self
        Some(sel) => {
          let cell = self.active().get_cell(sel.row, sel.col)
          { ..self, clipboard: { cells: [[cell]], origin: sel } }
        }
      }
  }
}

///|
pub fn Workbook::copy_range(
  self : Workbook,
  start_row : Int,
  start_col : Int,
  end_row : Int,
  end_col : Int,
) -> Workbook {
  let sheet = self.active()
  let row_start = clamp_int(min_int(start_row, end_row), 0, sheet.row_count - 1)
  let row_end = clamp_int(max_int(start_row, end_row), 0, sheet.row_count - 1)
  let col_start = clamp_int(min_int(start_col, end_col), 0, sheet.col_count - 1)
  let col_end = clamp_int(max_int(start_col, end_col), 0, sheet.col_count - 1)
  let rows : Array[Array[@cell.Cell]] = []
  for row in row_start..<=row_end {
    let cells : Array[@cell.Cell] = []
    for col in col_start..<=col_end {
      cells.push(sheet.get_cell(row, col))
    }
    rows.push(cells)
  }
  {
    ..self,
    clipboard: {
      cells: rows,
      origin: Selection::new(row=row_start, col=col_start),
    },
  }
}

///|
pub fn Workbook::has_clipboard(self : Workbook) -> Bool {
  !self.clipboard.cells.is_empty()
}

///|
pub fn Workbook::clipboard_tsv(self : Workbook) -> String {
  let rows : Array[String] = []
  for row_cells in self.clipboard.cells {
    let values : Array[String] = []
    for cell in row_cells {
      values.push(tsv_escape_cell(cell.display()))
    }
    rows.push(values.join("\t"))
  }
  rows.join("\n")
}

///|
pub fn Workbook::cut_selected(self : Workbook) -> Workbook {
  self.copy_selected().clear_selected()
}

///|
pub fn Workbook::paste_selected(self : Workbook) -> Workbook {
  match self.selection {
    None => self
    Some(sel) => {
      let changes : Array[CellChange] = []
      let mut sheet = self.active()
      let row_count = self.clipboard.cells.length()
      let mut col_count = 0
      for row_cells in self.clipboard.cells {
        col_count = max_int(col_count, row_cells.length())
      }
      for row_offset, row_cells in self.clipboard.cells {
        for col_offset, source_cell in row_cells {
          let row = sel.row + row_offset
          let col = sel.col + col_offset
          let old_cell = sheet.get_cell(row, col)
          let next_cell = adjust_pasted_cell(
            source_cell,
            drow=row - (self.clipboard.origin.row + row_offset),
            dcol=col - (self.clipboard.origin.col + col_offset),
          )
          sheet = sheet.set_cell_no_undo(row, col, next_cell)
          if old_cell != next_cell {
            changes.push({ row, col, old_cell, new_cell: next_cell })
          }
        }
      }
      let next = self
        .with_active_sheet(sheet.recalc())
        .select_pasted_area(sel, row_count, col_count)
      if changes.is_empty() {
        next
      } else if changes.length() == 1 {
        let change = changes[0]
        next.push_undo(
          UndoOp::CellEdit(
            change.row,
            change.col,
            change.old_cell,
            change.new_cell,
          ),
        )
      } else {
        next.push_undo(UndoOp::CellBatchEdit(changes))
      }
    }
  }
}

///|
pub fn Workbook::paste_tsv(self : Workbook, text : String) -> Workbook {
  match self.selection {
    None => self
    Some(sel) => {
      let rows = parse_tsv_rows(text)
      if rows.is_empty() {
        return self
      }
      let row_count = rows.length()
      let mut col_count = 0
      for row_values in rows {
        col_count = max_int(col_count, row_values.length())
      }
      let changes : Array[CellChange] = []
      let mut sheet = self.active()
      for row_offset, row_values in rows {
        for col_offset, raw in row_values {
          let row = sel.row + row_offset
          let col = sel.col + col_offset
          let old_cell = sheet.get_cell(row, col)
          let next_cell = { ..old_cell, raw, value: @cell.parse_raw_value(raw) }
          sheet = sheet.set_cell_no_undo(row, col, next_cell)
          if old_cell != next_cell {
            changes.push({ row, col, old_cell, new_cell: next_cell })
          }
        }
      }
      let next = self
        .with_active_sheet(sheet.recalc())
        .select_pasted_area(sel, row_count, col_count)
      if changes.is_empty() {
        next
      } else if changes.length() == 1 {
        let change = changes[0]
        next.push_undo(
          UndoOp::CellEdit(
            change.row,
            change.col,
            change.old_cell,
            change.new_cell,
          ),
        )
      } else {
        next.push_undo(UndoOp::CellBatchEdit(changes))
      }
    }
  }
}

///|
fn Workbook::select_pasted_area(
  self : Workbook,
  anchor : Selection,
  row_count : Int,
  col_count : Int,
) -> Workbook {
  if row_count <= 0 || col_count <= 0 {
    self
  } else if row_count == 1 && col_count == 1 {
    self.select_cell(anchor.row, anchor.col)
  } else {
    self.select_range(
      anchor.row,
      anchor.col,
      anchor.row + row_count - 1,
      anchor.col + col_count - 1,
    )
  }
}

///|
fn parse_tsv_rows(text : String) -> Array[Array[String]] {
  if text.is_empty() {
    return []
  }
  let rows : Array[Array[String]] = []
  let chars = text.to_array()
  let mut row : Array[String] = []
  let cell = StringBuilder()
  let mut quoted = false
  let mut ended_with_row_separator = false
  let mut index = 0
  while index < chars.length() {
    let ch = chars[index]
    if quoted {
      if ch == '"' {
        if index + 1 < chars.length() && chars[index + 1] == '"' {
          cell.write_char('"')
          index = index + 2
        } else {
          quoted = false
          index = index + 1
        }
      } else {
        cell.write_char(ch)
        index = index + 1
      }
    } else if ch == '"' && cell.is_empty() {
      quoted = true
      ended_with_row_separator = false
      index = index + 1
    } else if ch == '\t' {
      row.push(cell.to_string())
      cell.reset()
      ended_with_row_separator = false
      index = index + 1
    } else if ch == '\n' {
      row.push(cell.to_string())
      rows.push(row)
      row = []
      cell.reset()
      ended_with_row_separator = true
      index = index + 1
    } else if ch == '\r' {
      row.push(cell.to_string())
      rows.push(row)
      row = []
      cell.reset()
      ended_with_row_separator = true
      if index + 1 < chars.length() && chars[index + 1] == '\n' {
        index = index + 2
      } else {
        index = index + 1
      }
    } else {
      cell.write_char(ch)
      ended_with_row_separator = false
      index = index + 1
    }
  }
  if !(ended_with_row_separator && row.is_empty() && cell.is_empty()) {
    row.push(cell.to_string())
    rows.push(row)
  }
  rows
}

///|
fn tsv_escape_cell(value : String) -> String {
  if !(value.contains("\t") ||
    value.contains("\n") ||
    value.contains("\r") ||
    value.contains("\"")) {
    return value
  }
  let builder = StringBuilder()
  builder.write_char('"')
  for ch in value {
    if ch == '"' {
      builder.write_char('"')
      builder.write_char('"')
    } else {
      builder.write_char(ch)
    }
  }
  builder.write_char('"')
  builder.to_string()
}

///|
pub fn Workbook::sort_selected_by_first_column(
  self : Workbook,
  ascending~ : Bool,
) -> Workbook {
  match self.selection_range {
    None => self
    Some(range) => {
      if range.start.row >= range.end.row {
        return self
      }
      let sheet = self.active()
      let rows : Array[SortRow] = []
      for row in range.start.row..<=range.end.row {
        let cells : Array[@cell.Cell] = []
        for col in range.start.col..<=range.end.col {
          cells.push(sheet.get_cell(row, col))
        }
        rows.push({ source_row: row, cells })
      }
      rows.sort_by(fn(left, right) {
        compare_sort_rows(left, right, ascending~)
      })
      let changes : Array[CellChange] = []
      let mut updated_sheet = sheet
      for row_offset, sort_row in rows {
        let target_row = range.start.row + row_offset
        for col_offset, next_cell in sort_row.cells {
          let col = range.start.col + col_offset
          let old_cell = updated_sheet.get_cell(target_row, col)
          if old_cell != next_cell {
            updated_sheet = updated_sheet.set_cell_no_undo(
              target_row, col, next_cell,
            )
            changes.push({ row: target_row, col, old_cell, new_cell: next_cell })
          }
        }
      }
      let next = self.with_active_sheet(updated_sheet.recalc())
      if changes.is_empty() {
        next
      } else if changes.length() == 1 {
        let change = changes[0]
        next.push_undo(
          UndoOp::CellEdit(
            change.row,
            change.col,
            change.old_cell,
            change.new_cell,
          ),
        )
      } else {
        next.push_undo(UndoOp::CellBatchEdit(changes))
      }
    }
  }
}

///|
fn compare_sort_rows(left : SortRow, right : SortRow, ascending~ : Bool) -> Int {
  let left_key = left.cells[0]
  let right_key = right.cells[0]
  let left_blank = sort_cell_is_blank(left_key)
  let right_blank = sort_cell_is_blank(right_key)
  if left_blank && right_blank {
    left.source_row.compare(right.source_row)
  } else if left_blank {
    1
  } else if right_blank {
    -1
  } else {
    let value_order = compare_sort_cells(left_key, right_key)
    let directed = if ascending { value_order } else { -value_order }
    if directed == 0 {
      left.source_row.compare(right.source_row)
    } else {
      directed
    }
  }
}

///|
fn sort_cell_is_blank(cell : @cell.Cell) -> Bool {
  cell.raw.trim().to_owned().is_empty() || cell.value is @cell.CellValue::Empty
}

///|
fn compare_sort_cells(left : @cell.Cell, right : @cell.Cell) -> Int {
  match (left.value, right.value) {
    (@cell.CellValue::Number(a), @cell.CellValue::Number(b)) =>
      compare_double(a, b)
    (@cell.CellValue::Text(a), @cell.CellValue::Text(b)) => compare_text(a, b)
    (@cell.CellValue::Error(a), @cell.CellValue::Error(b)) => compare_text(a, b)
    (left_value, right_value) => {
      let rank_order = sort_value_rank(left_value).compare(
        sort_value_rank(right_value),
      )
      if rank_order == 0 {
        compare_text(left.display(), right.display())
      } else {
        rank_order
      }
    }
  }
}

///|
fn sort_value_rank(value : @cell.CellValue) -> Int {
  match value {
    @cell.CellValue::Number(_) => 0
    @cell.CellValue::Text(_) => 1
    @cell.CellValue::Error(_) => 2
    @cell.CellValue::Empty => 3
  }
}

///|
fn compare_double(left : Double, right : Double) -> Int {
  if left < right {
    -1
  } else if left > right {
    1
  } else {
    0
  }
}

///|
fn compare_text(left : String, right : String) -> Int {
  let primary = left.compare_ignore_ascii_case(right)
  if primary == 0 {
    left.compare(right)
  } else {
    primary
  }
}

///|
pub fn Workbook::fill_down_selected(self : Workbook) -> Workbook {
  match self.selection_range {
    None => self
    Some(range) => {
      if range.start.row >= range.end.row {
        return self
      }
      let sheet = self.active()
      let changes : Array[CellChange] = []
      let mut updated_sheet = sheet
      for row in (range.start.row + 1)..<=range.end.row {
        for col in range.start.col..<=range.end.col {
          let source_cell = sheet.get_cell(range.start.row, col)
          let old_cell = updated_sheet.get_cell(row, col)
          let next_cell = adjust_pasted_cell(
            source_cell,
            drow=row - range.start.row,
            dcol=0,
          )
          if old_cell != next_cell {
            updated_sheet = updated_sheet.set_cell_no_undo(row, col, next_cell)
            changes.push({ row, col, old_cell, new_cell: next_cell })
          }
        }
      }
      let next = self.with_active_sheet(updated_sheet.recalc())
      if changes.is_empty() {
        next
      } else if changes.length() == 1 {
        let change = changes[0]
        next.push_undo(
          UndoOp::CellEdit(
            change.row,
            change.col,
            change.old_cell,
            change.new_cell,
          ),
        )
      } else {
        next.push_undo(UndoOp::CellBatchEdit(changes))
      }
    }
  }
}

///|
pub fn Workbook::fill_right_selected(self : Workbook) -> Workbook {
  match self.selection_range {
    None => self
    Some(range) => {
      if range.start.col >= range.end.col {
        return self
      }
      let sheet = self.active()
      let changes : Array[CellChange] = []
      let mut updated_sheet = sheet
      for row in range.start.row..<=range.end.row {
        for col in (range.start.col + 1)..<=range.end.col {
          let source_cell = sheet.get_cell(row, range.start.col)
          let old_cell = updated_sheet.get_cell(row, col)
          let next_cell = adjust_pasted_cell(
            source_cell,
            drow=0,
            dcol=col - range.start.col,
          )
          if old_cell != next_cell {
            updated_sheet = updated_sheet.set_cell_no_undo(row, col, next_cell)
            changes.push({ row, col, old_cell, new_cell: next_cell })
          }
        }
      }
      let next = self.with_active_sheet(updated_sheet.recalc())
      if changes.is_empty() {
        next
      } else if changes.length() == 1 {
        let change = changes[0]
        next.push_undo(
          UndoOp::CellEdit(
            change.row,
            change.col,
            change.old_cell,
            change.new_cell,
          ),
        )
      } else {
        next.push_undo(UndoOp::CellBatchEdit(changes))
      }
    }
  }
}

///|
pub fn Workbook::insert_row(self : Workbook, index : Int) -> Workbook {
  let sheet = self.active()
  let idx = clamp_int(index, 0, sheet.row_count)
  self
  .with_active_sheet(shift_sheet_rows(sheet, idx, 1))
  .push_undo(UndoOp::RowInsert(idx))
}

///|
pub fn Workbook::delete_row(self : Workbook, index : Int) -> Workbook {
  let sheet = self.active()
  if index < 0 || index >= sheet.row_count {
    self
  } else {
    let deleted = collect_row_cells(sheet, index)
    self
    .with_active_sheet(shift_sheet_rows(sheet, index, -1))
    .push_undo(UndoOp::RowDelete(index, deleted))
  }
}

///|
pub fn Workbook::insert_column(self : Workbook, index : Int) -> Workbook {
  let sheet = self.active()
  let idx = clamp_int(index, 0, sheet.col_count)
  self
  .with_active_sheet(shift_sheet_cols(sheet, idx, 1))
  .push_undo(UndoOp::ColumnInsert(idx))
}

///|
pub fn Workbook::delete_column(self : Workbook, index : Int) -> Workbook {
  let sheet = self.active()
  if index < 0 || index >= sheet.col_count {
    self
  } else {
    let deleted = collect_column_cells(sheet, index)
    self
    .with_active_sheet(shift_sheet_cols(sheet, index, -1))
    .push_undo(UndoOp::ColumnDelete(index, deleted))
  }
}

///|
pub fn Workbook::add_sheet(self : Workbook) -> Workbook {
  let sheets = self.sheets.copy()
  let name = next_sheet_name(sheets)
  sheets.push(Sheet::new(name))
  {
    ..self,
    sheets,
    active_sheet: sheets.length() - 1,
    selection: None,
    selection_range: None,
    undo_stack: [],
    redo_stack: [],
  }
}

///|
fn next_sheet_name(sheets : Array[Sheet]) -> String {
  let mut index = sheets.length() + 1
  while true {
    let name = "Sheet " + index.to_string()
    if !sheets.any(sheet => sheet.name == name) {
      return name
    }
    index = index + 1
  }
  "Sheet"
}

///|
pub fn Workbook::delete_sheet(self : Workbook, index : Int) -> Workbook {
  if self.sheets.length() <= 1 || index < 0 || index >= self.sheets.length() {
    self
  } else {
    let sheets = self.sheets.copy()
    ignore(sheets.remove(index))
    let active_sheet = active_sheet_after_delete(
      self.active_sheet,
      index,
      sheets.length(),
    )
    {
      ..self,
      sheets,
      active_sheet,
      selection: None,
      selection_range: None,
      undo_stack: [],
      redo_stack: [],
    }
  }
}

///|
fn active_sheet_after_delete(
  active : Int,
  deleted : Int,
  next_len : Int,
) -> Int {
  if deleted < active {
    active - 1
  } else if deleted == active && active >= next_len {
    next_len - 1
  } else {
    active
  }
}

///|
pub fn Workbook::undo(self : Workbook) -> Workbook {
  match self.undo_stack.last() {
    None => self
    Some(op) => {
      let undo_stack = self.undo_stack.copy()
      ignore(undo_stack.pop())
      let redo_stack = self.redo_stack.copy()
      redo_stack.push(op)
      let result = match op {
        UndoOp::CellEdit(row, col, old_cell, _new_cell) =>
          self.with_active_sheet(
            self.active().set_cell_no_undo(row, col, old_cell).recalc(),
          )
        UndoOp::CellBatchEdit(changes) =>
          self.with_active_sheet(
            apply_cell_changes(self.active(), changes, old=true),
          )
        UndoOp::CellFormat(row, col, old_format, _new_format) =>
          self.with_active_sheet(
            self.active().set_format_no_undo(row, col, old_format),
          )
        UndoOp::RowInsert(index) =>
          self.with_active_sheet(shift_sheet_rows(self.active(), index, -1))
        UndoOp::ColumnInsert(index) =>
          self.with_active_sheet(shift_sheet_cols(self.active(), index, -1))
        UndoOp::RowDelete(index, cells) =>
          self.with_active_sheet(restore_row_cells(self.active(), index, cells))
        UndoOp::ColumnDelete(index, cells) =>
          self.with_active_sheet(
            restore_column_cells(self.active(), index, cells),
          )
      }
      { ..result, undo_stack, redo_stack }
    }
  }
}

///|
pub fn Workbook::redo(self : Workbook) -> Workbook {
  match self.redo_stack.last() {
    None => self
    Some(op) => {
      let redo_stack = self.redo_stack.copy()
      ignore(redo_stack.pop())
      let undo_stack = self.undo_stack.copy()
      undo_stack.push(op)
      let result = match op {
        UndoOp::CellEdit(row, col, _old_cell, new_cell) =>
          self.with_active_sheet(
            self.active().set_cell_no_undo(row, col, new_cell).recalc(),
          )
        UndoOp::CellBatchEdit(changes) =>
          self.with_active_sheet(
            apply_cell_changes(self.active(), changes, old=false),
          )
        UndoOp::CellFormat(row, col, _old_format, new_format) =>
          self.with_active_sheet(
            self.active().set_format_no_undo(row, col, new_format),
          )
        UndoOp::RowInsert(index) =>
          self.with_active_sheet(shift_sheet_rows(self.active(), index, 1))
        UndoOp::ColumnInsert(index) =>
          self.with_active_sheet(shift_sheet_cols(self.active(), index, 1))
        UndoOp::RowDelete(index, _cells) =>
          self.with_active_sheet(shift_sheet_rows(self.active(), index, -1))
        UndoOp::ColumnDelete(index, _cells) =>
          self.with_active_sheet(shift_sheet_cols(self.active(), index, -1))
      }
      { ..result, undo_stack, redo_stack }
    }
  }
}

///|
fn apply_cell_changes(
  sheet : Sheet,
  changes : Array[CellChange],
  old~ : Bool,
) -> Sheet {
  let mut result = sheet
  for change in changes {
    result = result.set_cell_no_undo(
      change.row,
      change.col,
      if old {
        change.old_cell
      } else {
        change.new_cell
      },
    )
  }
  result.recalc()
}

///|
pub fn Workbook::with_active_sheet(self : Workbook, sheet : Sheet) -> Workbook {
  let sheets = self.sheets.copy()
  sheets[self.active_sheet] = sheet
  { ..self, sheets, }
}

///|
pub fn Workbook::push_undo(self : Workbook, op : UndoOp) -> Workbook {
  let undo_stack = self.undo_stack.copy()
  undo_stack.push(op)
  { ..self, undo_stack, redo_stack: [] }
}

///|
fn shift_sheet_rows(sheet : Sheet, index : Int, delta : Int) -> Sheet {
  let cells : Map[String, @cell.Cell] = Map([])
  for key, cell in sheet.cells {
    match @cell.parse_ref(key) {
      Some(addr) =>
        if delta > 0 {
          let row = if addr.row >= index { addr.row + 1 } else { addr.row }
          cells[@cell.make_key(row, addr.col)] = adjust_cell_row_refs(
            cell, index, delta,
          )
        } else if addr.row == index {
          ()
        } else {
          let row = if addr.row > index { addr.row - 1 } else { addr.row }
          cells[@cell.make_key(row, addr.col)] = adjust_cell_row_refs(
            cell, index, delta,
          )
        }
      None => ()
    }
  }
  let row_count = if delta > 0 {
    sheet.row_count + 1
  } else {
    max_int(1, sheet.row_count - 1)
  }
  { ..sheet, row_count, cells }.recalc()
}

///|
fn collect_row_cells(sheet : Sheet, row : Int) -> Array[IndexedCell] {
  let cells : Array[IndexedCell] = []
  for key, cell in sheet.cells {
    match @cell.parse_ref(key) {
      Some(addr) => if addr.row == row { cells.push({ index: addr.col, cell }) }
      None => ()
    }
  }
  cells
}

///|
fn restore_row_cells(
  sheet : Sheet,
  row : Int,
  cells : Array[IndexedCell],
) -> Sheet {
  let mut result = shift_sheet_rows(sheet, row, 1)
  for item in cells {
    result = result.set_cell_no_undo(row, item.index, item.cell)
  }
  result.recalc()
}

///|
fn shift_sheet_cols(sheet : Sheet, index : Int, delta : Int) -> Sheet {
  let cells : Map[String, @cell.Cell] = Map([])
  for key, cell in sheet.cells {
    match @cell.parse_ref(key) {
      Some(addr) =>
        if delta > 0 {
          let col = if addr.col >= index { addr.col + 1 } else { addr.col }
          cells[@cell.make_key(addr.row, col)] = adjust_cell_col_refs(
            cell, index, delta,
          )
        } else if addr.col == index {
          ()
        } else {
          let col = if addr.col > index { addr.col - 1 } else { addr.col }
          cells[@cell.make_key(addr.row, col)] = adjust_cell_col_refs(
            cell, index, delta,
          )
        }
      None => ()
    }
  }
  let col_count = if delta > 0 {
    sheet.col_count + 1
  } else {
    max_int(1, sheet.col_count - 1)
  }
  { ..sheet, col_count, cells }.recalc()
}

///|
fn collect_column_cells(sheet : Sheet, col : Int) -> Array[IndexedCell] {
  let cells : Array[IndexedCell] = []
  for key, cell in sheet.cells {
    match @cell.parse_ref(key) {
      Some(addr) => if addr.col == col { cells.push({ index: addr.row, cell }) }
      None => ()
    }
  }
  cells
}

///|
fn restore_column_cells(
  sheet : Sheet,
  col : Int,
  cells : Array[IndexedCell],
) -> Sheet {
  let mut result = shift_sheet_cols(sheet, col, 1)
  for item in cells {
    result = result.set_cell_no_undo(item.index, col, item.cell)
  }
  result.recalc()
}

///|
fn adjust_pasted_cell(
  cell : @cell.Cell,
  drow~ : Int,
  dcol~ : Int,
) -> @cell.Cell {
  { ..cell, raw: translate_formula_refs(cell.raw, drow~, dcol~) }
}

///|
fn translate_formula_refs(raw : String, drow~ : Int, dcol~ : Int) -> String {
  rewrite_formula_refs(raw, ref_token => {
    let row = if ref_token.row_abs {
      ref_token.addr.row
    } else {
      ref_token.addr.row + drow
    }
    let col = if ref_token.col_abs {
      ref_token.addr.col
    } else {
      ref_token.addr.col + dcol
    }
    if row < 0 || col < 0 {
      "#REF!"
    } else {
      format_ref_token(ref_token, row, col)
    }
  })
}

///|
fn adjust_cell_row_refs(
  cell : @cell.Cell,
  index : Int,
  delta : Int,
) -> @cell.Cell {
  {
    ..cell,
    raw: rewrite_formula_refs(cell.raw, ref_token => {
      shift_row_ref(ref_token, index, delta)
    }),
  }
}

///|
fn adjust_cell_col_refs(
  cell : @cell.Cell,
  index : Int,
  delta : Int,
) -> @cell.Cell {
  {
    ..cell,
    raw: rewrite_formula_refs(cell.raw, ref_token => {
      shift_col_ref(ref_token, index, delta)
    }),
  }
}

///|
fn shift_row_ref(token : FormulaRefToken, index : Int, delta : Int) -> String {
  let addr = token.addr
  if delta > 0 {
    let row = if addr.row >= index { addr.row + delta } else { addr.row }
    format_ref_token(token, row, addr.col)
  } else if addr.row == index {
    "#REF!"
  } else {
    let row = if addr.row > index { addr.row + delta } else { addr.row }
    format_ref_token(token, row, addr.col)
  }
}

///|
fn shift_col_ref(token : FormulaRefToken, index : Int, delta : Int) -> String {
  let addr = token.addr
  if delta > 0 {
    let col = if addr.col >= index { addr.col + delta } else { addr.col }
    format_ref_token(token, addr.row, col)
  } else if addr.col == index {
    "#REF!"
  } else {
    let col = if addr.col > index { addr.col + delta } else { addr.col }
    format_ref_token(token, addr.row, col)
  }
}

///|
fn rewrite_formula_refs(
  raw : String,
  rewrite : (FormulaRefToken) -> String,
) -> String {
  if raw.length() <= 1 || raw[0] != '=' {
    return raw
  }
  let chars = raw.to_array()
  let builder = StringBuilder()
  let mut i = 0
  while i < chars.length() {
    if is_ascii_letter(chars[i]) || chars[i] == '$' {
      let start = i
      let mut col_abs = false
      if chars[i] == '$' {
        col_abs = true
        i = i + 1
      }
      let col_start = i
      while i < chars.length() && is_ascii_letter(chars[i]) {
        i = i + 1
      }
      let col_end = i
      let mut row_abs = false
      if i < chars.length() && chars[i] == '$' {
        row_abs = true
        i = i + 1
      }
      let row_start = i
      while i < chars.length() && is_ascii_digit(chars[i]) {
        i = i + 1
      }
      let token = chars_to_string(chars[start:i])
      if col_start < col_end && row_start < i {
        match @cell.parse_ref(token) {
          Some(addr) =>
            builder.write_string(rewrite({ addr, col_abs, row_abs }))
          None => builder.write_string(token)
        }
      } else {
        builder.write_string(token)
      }
    } else {
      builder.write_char(chars[i])
      i = i + 1
    }
  }
  builder.to_string()
}

///|
fn format_ref_token(token : FormulaRefToken, row : Int, col : Int) -> String {
  let col_prefix = if token.col_abs { "$" } else { "" }
  let row_prefix = if token.row_abs { "$" } else { "" }
  col_prefix + @cell.column_label(col) + row_prefix + (row + 1).to_string()
}

///|
fn chars_to_string(chars : ArrayView[Char]) -> String {
  let builder = StringBuilder()
  for ch in chars {
    builder.write_char(ch)
  }
  builder.to_string()
}

///|
fn is_ascii_letter(c : Char) -> Bool {
  (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z')
}

///|
fn is_ascii_digit(c : Char) -> Bool {
  c >= '0' && c <= '9'
}

///|
fn clamp_int(value : Int, min : Int, max : Int) -> Int {
  if value < min {
    min
  } else if value > max {
    max
  } else {
    value
  }
}

///|
fn Sheet::data_edge_position(
  self : Sheet,
  row : Int,
  col : Int,
  drow : Int,
  dcol : Int,
) -> Selection {
  let row_dir = sign_int(drow)
  let col_dir = sign_int(dcol)
  if (row_dir == 0 && col_dir == 0) || (row_dir != 0 && col_dir != 0) {
    Selection::new(
      row=clamp_int(row, 0, self.row_count - 1),
      col=clamp_int(col, 0, self.col_count - 1),
    )
  } else {
    let start_row = clamp_int(row, 0, self.row_count - 1)
    let start_col = clamp_int(col, 0, self.col_count - 1)
    let current_empty = self.get_cell(start_row, start_col).is_empty()
    let next_row = start_row + row_dir
    let next_col = start_col + col_dir
    if !self.in_bounds(next_row, next_col) {
      Selection::new(row=start_row, col=start_col)
    } else if current_empty {
      self.first_non_empty_or_boundary(next_row, next_col, row_dir, col_dir)
    } else if self.get_cell(next_row, next_col).is_empty() {
      self.first_non_empty_or_boundary(next_row, next_col, row_dir, col_dir)
    } else {
      self.last_contiguous_non_empty(next_row, next_col, row_dir, col_dir)
    }
  }
}

///|
fn Sheet::first_non_empty_or_boundary(
  self : Sheet,
  row : Int,
  col : Int,
  row_dir : Int,
  col_dir : Int,
) -> Selection {
  let mut edge_row = row
  let mut edge_col = col
  let mut probe_row = row
  let mut probe_col = col
  while self.in_bounds(probe_row, probe_col) &&
        self.get_cell(probe_row, probe_col).is_empty() {
    edge_row = probe_row
    edge_col = probe_col
    probe_row = probe_row + row_dir
    probe_col = probe_col + col_dir
  }
  if self.in_bounds(probe_row, probe_col) {
    Selection::new(row=probe_row, col=probe_col)
  } else {
    Selection::new(row=edge_row, col=edge_col)
  }
}

///|
fn Sheet::last_contiguous_non_empty(
  self : Sheet,
  row : Int,
  col : Int,
  row_dir : Int,
  col_dir : Int,
) -> Selection {
  let mut edge_row = row
  let mut edge_col = col
  let mut probe_row = row + row_dir
  let mut probe_col = col + col_dir
  while self.in_bounds(probe_row, probe_col) &&
        !self.get_cell(probe_row, probe_col).is_empty() {
    edge_row = probe_row
    edge_col = probe_col
    probe_row = probe_row + row_dir
    probe_col = probe_col + col_dir
  }
  Selection::new(row=edge_row, col=edge_col)
}

///|
fn Sheet::in_bounds(self : Sheet, row : Int, col : Int) -> Bool {
  row >= 0 && row < self.row_count && col >= 0 && col < self.col_count
}

///|
fn sign_int(value : Int) -> Int {
  if value < 0 {
    -1
  } else if value > 0 {
    1
  } else {
    0
  }
}

///|
fn max_int(a : Int, b : Int) -> Int {
  if a > b {
    a
  } else {
    b
  }
}

///|
fn min_int(a : Int, b : Int) -> Int {
  if a < b {
    a
  } else {
    b
  }
}