tuitab 0.4.2

Terminal tabular data explorer — CSV/JSON/Parquet/Excel/SQLite viewer with filtering, sorting, pivot tables, and charts
use crate::app::App;
use crate::types::{Action, AppMode, CopyPending};
use polars::prelude::*;
use ratatui::widgets::ScrollbarState;

impl App {
    pub(crate) fn handle_clipboard_action(&mut self, action: Action) -> Option<Action> {
        match action {
            Action::EnterYPrefix => {
                self.mode = AppMode::YPrefix;
                self.status_message =
                    "y: (c)cell  (r)rows  (z)col.values  (Z)whole col  (R)whole table  Esc=cancel"
                        .to_string();
                None
            }
            Action::CancelYPrefix => {
                self.mode = AppMode::Normal;
                self.status_message.clear();
                None
            }
            Action::CopyCurrentCell => {
                let s = self.stack.active();
                let row = s.table_state.selected().unwrap_or(0);
                let col = s.cursor_col;
                let phys = s.dataframe.row_order.get(row).copied().unwrap_or(0);
                let val = s.dataframe.format_display(phys, col);
                match crate::clipboard::copy_text(&val) {
                    Ok(_) => self.status_message = format!("Copied cell value: {}", val),
                    Err(e) => self.status_message = format!("Clipboard error: {}", e),
                }
                self.mode = AppMode::Normal;
                None
            }
            Action::OpenCopyFormat(pending) => {
                if pending == CopyPending::SmartColumn
                    && self.stack.active().dataframe.selected_rows.is_empty()
                {
                    let s = self.stack.active();
                    let row = s.table_state.selected().unwrap_or(0);
                    let phys = s.dataframe.row_order.get(row).copied().unwrap_or(0);
                    let val = s.dataframe.format_display(phys, s.cursor_col);
                    self.status_message = match crate::clipboard::copy_text(&val) {
                        Ok(_) => format!("Copied cell value: {}", val),
                        Err(e) => format!("Clipboard error: {}", e),
                    };
                    self.mode = AppMode::Normal;
                } else {
                    self.copy.pending = Some(pending);
                    self.copy.format_index = 0;
                    self.mode = AppMode::CopyFormatSelect;
                }
                None
            }
            Action::CopyFormatSelectUp => {
                if self.copy.format_index > 0 {
                    self.copy.format_index -= 1;
                }
                None
            }
            Action::CopyFormatSelectDown => {
                let max = self.copy_format_option_count().saturating_sub(1);
                if self.copy.format_index < max {
                    self.copy.format_index += 1;
                }
                None
            }
            Action::CancelCopyFormat => {
                self.copy.pending = None;
                self.mode = AppMode::Normal;
                self.status_message.clear();
                None
            }
            Action::ApplyCopyFormat => {
                match self.execute_copy_with_format() {
                    Ok(msg) => self.status_message = msg,
                    Err(e) => self.status_message = format!("Clipboard error: {}", e),
                }
                self.copy.pending = None;
                self.mode = AppMode::Normal;
                None
            }
            Action::PasteRows => {
                self.paste_rows();
                None
            }
            other => Some(other),
        }
    }

    pub(super) fn copy_format_option_count(&self) -> usize {
        match self.copy.pending {
            Some(CopyPending::SmartRows | CopyPending::WholeTable) => 4,
            Some(CopyPending::SmartColumn | CopyPending::WholeColumn) => 3,
            None => 0,
        }
    }

    fn effective_col_indices(df: &crate::data::dataframe::DataFrame) -> Vec<usize> {
        let selected: Vec<usize> = df
            .columns
            .iter()
            .enumerate()
            .filter(|(_, c)| c.selected)
            .map(|(i, _)| i)
            .collect();
        if selected.is_empty() {
            (0..df.col_count()).collect()
        } else {
            selected
        }
    }

    pub(super) fn execute_copy_with_format(&self) -> color_eyre::Result<String> {
        let s = self.stack.active();
        let df = &s.dataframe;
        match self.copy.pending {
            Some(CopyPending::SmartRows) => {
                let col_indices = Self::effective_col_indices(df);
                let headers: Vec<&str> = col_indices
                    .iter()
                    .map(|&i| df.columns[i].name.as_str())
                    .collect();
                if df.selected_rows.is_empty() {
                    let row = s.table_state.selected().unwrap_or(0);
                    let phys = df.row_order.get(row).copied().unwrap_or(0);
                    let row_data: Vec<String> = col_indices
                        .iter()
                        .map(|&c| df.format_display(phys, c))
                        .collect();
                    let rows = vec![row_data];
                    self.copy_rows_with_format(&headers, &rows)
                        .map(|fmt| format!("Copied row ({})", fmt))
                } else {
                    let mut sorted_phys: Vec<usize> = df.selected_rows.iter().copied().collect();
                    sorted_phys.sort_unstable();
                    let rows: Vec<Vec<String>> = sorted_phys
                        .iter()
                        .map(|&phys| {
                            col_indices
                                .iter()
                                .map(|&c| df.format_display(phys, c))
                                .collect()
                        })
                        .collect();
                    let count = rows.len();
                    self.copy_rows_with_format(&headers, &rows)
                        .map(|fmt| format!("Copied {} rows ({})", count, fmt))
                }
            }
            Some(CopyPending::SmartColumn) => {
                let col = s.cursor_col;
                let mut sorted_phys: Vec<usize> = df.selected_rows.iter().copied().collect();
                sorted_phys.sort_unstable();
                let values: Vec<String> = sorted_phys
                    .iter()
                    .map(|&phys| df.format_display(phys, col))
                    .collect();
                let count = values.len();
                self.copy_column_with_format(&values)
                    .map(|fmt| format!("Copied {} values ({})", count, fmt))
            }
            Some(CopyPending::WholeColumn) => {
                let col = s.cursor_col;
                let values: Vec<String> = (0..df.visible_row_count())
                    .map(|r| df.format_display(df.row_order[r], col))
                    .collect();
                let count = values.len();
                self.copy_column_with_format(&values)
                    .map(|fmt| format!("Copied {} values ({})", count, fmt))
            }
            Some(CopyPending::WholeTable) => {
                let col_indices = Self::effective_col_indices(df);
                let headers: Vec<&str> = col_indices
                    .iter()
                    .map(|&i| df.columns[i].name.as_str())
                    .collect();
                let rows: Vec<Vec<String>> = (0..df.visible_row_count())
                    .map(|r| {
                        let phys = df.row_order[r];
                        col_indices
                            .iter()
                            .map(|&c| df.format_display(phys, c))
                            .collect()
                    })
                    .collect();
                let count = rows.len();
                self.copy_rows_with_format(&headers, &rows)
                    .map(|fmt| format!("Copied {} rows ({})", count, fmt))
            }
            None => Ok(String::new()),
        }
    }

    fn copy_rows_with_format(
        &self,
        headers: &[&str],
        rows: &[Vec<String>],
    ) -> color_eyre::Result<&'static str> {
        match self.copy.format_index {
            0 => {
                crate::clipboard::copy_tsv(headers, rows)?;
                Ok("TSV")
            }
            1 => {
                crate::clipboard::copy_csv(headers, rows)?;
                Ok("CSV")
            }
            2 => {
                crate::clipboard::copy_json(headers, rows)?;
                Ok("JSON")
            }
            _ => {
                crate::clipboard::copy_markdown(headers, rows)?;
                Ok("Markdown")
            }
        }
    }

    fn copy_column_with_format(&self, values: &[String]) -> color_eyre::Result<&'static str> {
        match self.copy.format_index {
            0 => {
                crate::clipboard::copy_column_newline(values)?;
                Ok("newline-separated")
            }
            1 => {
                crate::clipboard::copy_column_comma(values)?;
                Ok("comma-separated")
            }
            _ => {
                crate::clipboard::copy_column_comma_quoted(values)?;
                Ok("comma-separated, quoted")
            }
        }
    }

    pub(super) fn paste_rows(&mut self) {
        match crate::clipboard::paste_from_clipboard() {
            Ok(text) => {
                let s = self.stack.active_mut();
                s.push_undo();
                let df = &mut s.dataframe;
                let col_count = df.col_count();
                if col_count == 0 {
                    return;
                }
                let lines: Vec<&str> = text.lines().collect();
                if lines.is_empty() {
                    self.status_message = "Clipboard is empty".to_string();
                    return;
                }
                let start = if lines[0]
                    .split('\t')
                    .zip(df.columns.iter())
                    .all(|(a, b)| a == b.name)
                {
                    1
                } else {
                    0
                };

                let mut series_vec = Vec::new();
                for col in 0..col_count {
                    let mut col_data = Vec::new();
                    for line in &lines[start..] {
                        let fields: Vec<&str> = line.split('\t').collect();
                        let val = fields.get(col).unwrap_or(&"").to_string();
                        col_data.push(val);
                    }
                    let series = Series::new(df.columns[col].name.clone().into(), &col_data);
                    series_vec.push(series.into());
                }
                if let Ok(new_df) = polars::prelude::DataFrame::new_infer_height(series_vec) {
                    let original_height = df.df.height();
                    if original_height == 0 {
                        df.df = new_df;
                    } else {
                        let _ = df.df.vstack_mut(&new_df);
                    }
                    let added = lines.len() - start;
                    for i in 0..added {
                        let new_idx = original_height + i;
                        std::sync::Arc::make_mut(&mut df.row_order).push(new_idx);
                        std::sync::Arc::make_mut(&mut df.original_order).push(new_idx);
                    }
                    df.modified = true;
                    df.calc_widths(40, 1000);
                    let vis = df.visible_row_count();
                    s.scroll_state = ScrollbarState::new(vis.saturating_sub(1));
                    self.status_message = format!("Pasted {} rows", added);
                } else {
                    self.status_message = "Failed to create dataframe for paste".to_string();
                }
            }
            Err(e) => {
                self.status_message = format!("Clipboard error: {}", e);
            }
        }
    }
}