tuitab 0.5.0

Terminal tabular data explorer — CSV/JSON/YAML/TOML/Parquet/Excel/SQLite viewer with filtering, sorting, pivot tables, and charts
use crate::app::App;
use crate::types::{Action, AppMode};

impl App {
    pub(crate) fn handle_edit_action(&mut self, action: Action) -> Option<Action> {
        match action {
            Action::OpenExternalEditor => {
                self.open_in_editor_pending = true;
                None
            }
            Action::StartEdit => {
                self.start_edit();
                None
            }
            Action::EditInput(c) => {
                self.stack.active_mut().edit_input.insert_char(c);
                None
            }
            Action::EditBackspace => {
                self.stack.active_mut().edit_input.delete_backward();
                None
            }
            Action::EditForwardDelete => {
                self.stack.active_mut().edit_input.delete_forward();
                None
            }
            Action::EditCursorLeft => {
                self.stack.active_mut().edit_input.move_cursor_left();
                None
            }
            Action::EditCursorRight => {
                self.stack.active_mut().edit_input.move_cursor_right();
                None
            }
            Action::EditCursorStart => {
                self.stack.active_mut().edit_input.move_cursor_start();
                None
            }
            Action::EditCursorEnd => {
                self.stack.active_mut().edit_input.move_cursor_end();
                None
            }
            Action::ApplyEdit => {
                self.apply_edit();
                None
            }
            Action::CancelEdit => {
                self.mode = AppMode::Normal;
                self.status_message.clear();
                None
            }

            // ── Bulk edit (ge) ────────────────────────────────────────────────
            Action::StartBulkEdit => {
                self.start_bulk_edit();
                None
            }
            Action::BulkEditInput(c) => {
                self.stack.active_mut().edit_input.insert_char(c);
                None
            }
            Action::BulkEditBackspace => {
                self.stack.active_mut().edit_input.delete_backward();
                None
            }
            Action::BulkEditForwardDelete => {
                self.stack.active_mut().edit_input.delete_forward();
                None
            }
            Action::BulkEditCursorLeft => {
                self.stack.active_mut().edit_input.move_cursor_left();
                None
            }
            Action::BulkEditCursorRight => {
                self.stack.active_mut().edit_input.move_cursor_right();
                None
            }
            Action::BulkEditCursorStart => {
                self.stack.active_mut().edit_input.move_cursor_start();
                None
            }
            Action::BulkEditCursorEnd => {
                self.stack.active_mut().edit_input.move_cursor_end();
                None
            }
            Action::ApplyBulkEdit => {
                self.apply_bulk_edit();
                None
            }
            Action::CancelBulkEdit => {
                self.stack.active_mut().edit_input.clear();
                self.mode = AppMode::Normal;
                self.status_message.clear();
                None
            }

            other => Some(other),
        }
    }

    pub(super) fn start_edit(&mut self) {
        use crate::ui::text_input::TextInput;
        let s = self.stack.active_mut();
        if let Some(display_row) = s.table_state.selected() {
            if display_row < s.dataframe.visible_row_count() {
                let physical_row = s.dataframe.row_order[display_row];
                let col = s.cursor_col;
                s.edit_input =
                    TextInput::with_value(s.dataframe.get_physical(physical_row, col).to_string());
                s.edit_row = physical_row;
                s.edit_col = col;
                self.mode = AppMode::Editing;
            }
        }
    }

    pub(super) fn apply_edit(&mut self) {
        let s = self.stack.active_mut();
        s.push_undo();
        let mut new_value = s.edit_input.as_str().to_string();
        let row = s.edit_row;
        let col = s.edit_col;

        // Belt and braces: if anything reshaped the table behind the projection's back,
        // the index-based mapping no longer lines up and writing through it would modify
        // the wrong node.  Refuse rather than corrupt.
        if !s.doc_mapping_ok() {
            s.undo_stack.pop();
            self.mode = AppMode::Normal;
            self.status_message =
                "The table no longer matches the document — reopen the sheet".to_string();
            return;
        }

        // Doc-backed sheets write into the tree first; the table is only a projection,
        // and the tree decides how the edited value is typed and rendered back.
        if let Some(doc) = s.doc.as_mut() {
            match doc.set_cell(row, col, &new_value) {
                Ok(shown) => new_value = shown,
                Err(e) => {
                    s.undo_stack.pop();
                    self.mode = AppMode::Normal;
                    self.status_message = format!("Edit error: {}", e);
                    return;
                }
            }
        }

        let s = self.stack.active_mut();
        match s.dataframe.set_cell(row, col, new_value.clone()) {
            Ok(_) => {
                self.mode = AppMode::Normal;
                self.status_message = format!("Cell updated: '{}'", new_value);
            }
            Err(e) => {
                self.mode = AppMode::Normal;
                self.status_message = format!("Edit error: '{}'", e);
            }
        }
    }

    pub(super) fn start_bulk_edit(&mut self) {
        use crate::ui::text_input::TextInput;
        let s = self.stack.active_mut();
        if s.dataframe.selected_rows.is_empty() {
            self.mode = AppMode::Normal;
            self.status_message =
                "No rows selected. Use 's' or '|' to select rows first.".to_string();
            return;
        }
        let col = s.cursor_col;
        let initial = if let Some(display_row) = s.table_state.selected() {
            if display_row < s.dataframe.visible_row_count() {
                let phys = s.dataframe.row_order[display_row];
                s.dataframe.get_physical(phys, col)
            } else {
                String::new()
            }
        } else {
            String::new()
        };
        s.edit_input = TextInput::with_value(initial);
        s.edit_col = col;
        let count = s.dataframe.selected_rows.len();
        let col_name = s.dataframe.columns[col].name.clone();
        self.mode = AppMode::BulkEditing;
        self.status_message = format!("Bulk edit '{}' for {} selected rows: ", col_name, count);
    }

    pub(super) fn apply_bulk_edit(&mut self) {
        let s = self.stack.active_mut();
        let new_value = s.edit_input.as_str().to_string();
        let col = s.edit_col;
        if s.dataframe.selected_rows.is_empty() {
            s.edit_input.clear();
            self.mode = AppMode::Normal;
            self.status_message = "No rows selected".to_string();
            return;
        }
        s.push_undo();
        let selected = s.dataframe.selected_rows.clone();

        if !s.doc_mapping_ok() {
            s.pop_undo();
            s.redo_stack.pop();
            s.edit_input.clear();
            self.mode = AppMode::Normal;
            self.status_message =
                "The table no longer matches the document — reopen the sheet".to_string();
            return;
        }

        // On a doc-backed sheet every selected row is a separate node, so the tree is
        // updated row by row before the table is patched in bulk.
        if let Some(doc) = s.doc.as_mut() {
            let mut failed: Option<String> = None;
            for &row in &selected {
                if let Err(e) = doc.set_cell(row, col, &new_value) {
                    failed = Some(e.to_string());
                    break;
                }
            }
            if let Some(e) = failed {
                s.pop_undo();
                s.redo_stack.pop();
                s.edit_input.clear();
                self.mode = AppMode::Normal;
                self.status_message = format!("Bulk edit error: {}", e);
                return;
            }
        }

        let result = s
            .dataframe
            .set_cells_bulk(&selected, col, new_value.clone());
        s.edit_input.clear();
        self.mode = AppMode::Normal;
        let col_name = s.dataframe.columns[col].name.clone();
        self.status_message = match result {
            Ok(updated) => format!(
                "Bulk-edited {} cells in '{}' to '{}'",
                updated, col_name, new_value
            ),
            Err(e) => format!("Bulk edit error in '{}': {}", col_name, e),
        };
    }
}