sql_cli/data/
datatable_view.rs

1use crate::datatable::{DataTable, DataValue};
2use crossterm::event::{KeyCode, KeyEvent};
3use ratatui::layout::Constraint;
4use ratatui::style::{Color, Modifier, Style};
5use ratatui::widgets::{Block, Borders, Cell, Paragraph, Row, Table};
6
7/// Represents how data should be sorted
8#[derive(Debug, Clone)]
9pub struct SortConfig {
10    pub column_index: usize,
11    pub order: SortOrder,
12}
13
14#[derive(Debug, Clone, PartialEq)]
15pub enum SortOrder {
16    Ascending,
17    Descending,
18}
19
20/// Filter configuration for the view
21#[derive(Debug, Clone)]
22pub struct FilterConfig {
23    pub pattern: String,
24    pub column_index: Option<usize>, // None = search all columns
25    pub case_sensitive: bool,
26}
27
28/// Search state within the current view
29#[derive(Debug, Clone)]
30pub struct SearchState {
31    pub pattern: String,
32    pub current_match: Option<(usize, usize)>, // (row, col)
33    pub matches: Vec<(usize, usize)>,
34    pub case_sensitive: bool,
35}
36
37/// Current view mode for input handling
38#[derive(Debug, Clone, PartialEq)]
39pub enum ViewMode {
40    Normal,    // Normal navigation
41    Filtering, // User is typing a filter
42    Searching, // User is typing a search
43    Sorting,   // User is selecting sort column
44}
45
46/// Simple input for view operations (not full query input)
47#[derive(Debug, Clone)]
48pub struct SimpleInput {
49    pub text: String,
50    pub cursor_position: usize,
51}
52
53impl SimpleInput {
54    pub fn new() -> Self {
55        Self {
56            text: String::new(),
57            cursor_position: 0,
58        }
59    }
60
61    pub fn clear(&mut self) {
62        self.text.clear();
63        self.cursor_position = 0;
64    }
65
66    pub fn insert_char(&mut self, ch: char) {
67        self.text.insert(self.cursor_position, ch);
68        self.cursor_position += 1;
69    }
70
71    pub fn delete_char(&mut self) {
72        if self.cursor_position > 0 {
73            self.cursor_position -= 1;
74            self.text.remove(self.cursor_position);
75        }
76    }
77
78    pub fn move_cursor_left(&mut self) {
79        if self.cursor_position > 0 {
80            self.cursor_position -= 1;
81        }
82    }
83
84    pub fn move_cursor_right(&mut self) {
85        if self.cursor_position < self.text.len() {
86            self.cursor_position += 1;
87        }
88    }
89
90    pub fn move_cursor_home(&mut self) {
91        self.cursor_position = 0;
92    }
93
94    pub fn move_cursor_end(&mut self) {
95        self.cursor_position = self.text.len();
96    }
97}
98
99/// A view of a DataTable with presentation logic
100#[derive(Clone)]
101pub struct DataTableView {
102    /// The underlying data
103    table: DataTable,
104
105    /// Current view mode
106    mode: ViewMode,
107
108    /// View state
109    sort: Option<SortConfig>,
110    filter: Option<FilterConfig>,
111    search: Option<SearchState>,
112
113    /// View-specific inputs
114    filter_input: SimpleInput,
115    search_input: SimpleInput,
116
117    /// Derived/cached view data
118    pub visible_rows: Vec<usize>, // Row indices after filtering/sorting
119    pub column_widths: Vec<u16>, // Calculated column widths
120
121    /// Navigation state
122    pub selected_row: usize,
123    pub selected_col: usize,
124    pub scroll_offset: usize,     // First visible row (vertical)
125    pub horizontal_scroll: usize, // First visible column (horizontal)
126    pub page_size: usize,         // How many rows visible at once
127    pub visible_col_start: usize, // First visible column index
128    pub visible_col_end: usize,   // Last visible column index
129}
130
131impl DataTableView {
132    /// Create a new view from a DataTable
133    pub fn new(table: DataTable) -> Self {
134        let visible_rows: Vec<usize> = (0..table.row_count()).collect();
135        let column_widths = Self::calculate_column_widths(&table, &visible_rows);
136        let column_count = table.column_count();
137
138        Self {
139            table,
140            mode: ViewMode::Normal,
141            sort: None,
142            filter: None,
143            search: None,
144            filter_input: SimpleInput::new(),
145            search_input: SimpleInput::new(),
146            visible_rows,
147            column_widths,
148            selected_row: 0,
149            selected_col: 0,
150            scroll_offset: 0,
151            horizontal_scroll: 0,
152            page_size: 30, // Show more rows like enhanced TUI
153            visible_col_start: 0,
154            visible_col_end: column_count, // Start by showing all, will adjust dynamically
155        }
156    }
157
158    /// Get the underlying table
159    pub fn table(&self) -> &DataTable {
160        &self.table
161    }
162
163    /// Get the underlying table (mutable)
164    pub fn table_mut(&mut self) -> &mut DataTable {
165        &mut self.table
166    }
167
168    /// V47: Get reference to DataTable (for BufferAPI compatibility)
169    pub fn get_datatable(&self) -> &DataTable {
170        &self.table
171    }
172
173    /// V47: Get mutable reference to DataTable (for BufferAPI compatibility)
174    pub fn get_datatable_mut(&mut self) -> &mut DataTable {
175        &mut self.table
176    }
177
178    /// Update visible columns based on terminal width and height
179    pub fn update_viewport(&mut self, terminal_width: u16, terminal_height: u16) {
180        // Calculate how many columns we can fit
181        let mut total_width = 0u16;
182        let mut end_col = self.visible_col_start;
183
184        for i in self.visible_col_start..self.column_widths.len() {
185            let col_width = self.column_widths[i];
186            if total_width + col_width + 1 > terminal_width.saturating_sub(2) {
187                break; // Won't fit
188            }
189            total_width += col_width + 1;
190            end_col = i + 1;
191        }
192
193        // Ensure we show at least one column
194        if end_col == self.visible_col_start && self.visible_col_start < self.column_widths.len() {
195            end_col = self.visible_col_start + 1;
196        }
197
198        self.visible_col_end = end_col;
199
200        // Update page size based on terminal height
201        // Typically we have 3 lines for query, 3 for status, 1 for help, 2 for borders = 9 lines of UI
202        self.page_size = (terminal_height.saturating_sub(9) as usize).max(10);
203    }
204
205    /// Get current view mode
206    pub fn mode(&self) -> ViewMode {
207        self.mode.clone()
208    }
209
210    /// Get visible row count after filtering
211    pub fn visible_row_count(&self) -> usize {
212        self.visible_rows.len()
213    }
214
215    /// Apply a filter to the view
216    pub fn apply_filter(
217        &mut self,
218        pattern: String,
219        column_index: Option<usize>,
220        case_sensitive: bool,
221    ) {
222        self.filter = Some(FilterConfig {
223            pattern: pattern.clone(),
224            column_index,
225            case_sensitive,
226        });
227
228        self.update_visible_rows();
229        self.selected_row = 0; // Reset selection
230        self.scroll_offset = 0;
231    }
232
233    /// Clear the current filter
234    pub fn clear_filter(&mut self) {
235        self.filter = None;
236        self.update_visible_rows();
237        self.selected_row = 0;
238        self.scroll_offset = 0;
239    }
240
241    /// Apply sorting to the view
242    pub fn apply_sort(&mut self, column_index: usize, order: SortOrder) {
243        self.sort = Some(SortConfig {
244            column_index,
245            order,
246        });
247        self.update_visible_rows();
248        self.selected_row = 0;
249        self.scroll_offset = 0;
250    }
251
252    /// Clear sorting
253    pub fn clear_sort(&mut self) {
254        self.sort = None;
255        self.update_visible_rows();
256    }
257
258    /// Start a search within the view
259    pub fn start_search(&mut self, pattern: String, case_sensitive: bool) {
260        let matches = self.find_matches(&pattern, case_sensitive);
261        let current_match = matches.first().copied();
262
263        self.search = Some(SearchState {
264            pattern,
265            current_match,
266            matches,
267            case_sensitive,
268        });
269
270        // Navigate to first match if found
271        if let Some((row_idx, _)) = current_match {
272            if let Some(visible_pos) = self.visible_rows.iter().position(|&r| r == row_idx) {
273                self.selected_row = visible_pos;
274                self.ensure_row_visible(visible_pos);
275            }
276        }
277    }
278
279    /// Navigate to next search match
280    pub fn next_search_match(&mut self) {
281        if let Some(ref mut search) = self.search {
282            if let Some(current) = search.current_match {
283                if let Some(current_idx) = search.matches.iter().position(|&m| m == current) {
284                    let next_idx = (current_idx + 1) % search.matches.len();
285                    search.current_match = search.matches.get(next_idx).copied();
286
287                    if let Some((row_idx, _)) = search.current_match {
288                        if let Some(visible_pos) =
289                            self.visible_rows.iter().position(|&r| r == row_idx)
290                        {
291                            self.selected_row = visible_pos;
292                            self.ensure_row_visible(visible_pos);
293                        }
294                    }
295                }
296            }
297        }
298    }
299
300    /// Navigate to previous search match
301    pub fn prev_search_match(&mut self) {
302        if let Some(ref mut search) = self.search {
303            if let Some(current) = search.current_match {
304                if let Some(current_idx) = search.matches.iter().position(|&m| m == current) {
305                    let prev_idx = if current_idx == 0 {
306                        search.matches.len() - 1
307                    } else {
308                        current_idx - 1
309                    };
310                    search.current_match = search.matches.get(prev_idx).copied();
311
312                    if let Some((row_idx, _)) = search.current_match {
313                        if let Some(visible_pos) =
314                            self.visible_rows.iter().position(|&r| r == row_idx)
315                        {
316                            self.selected_row = visible_pos;
317                            self.ensure_row_visible(visible_pos);
318                        }
319                    }
320                }
321            }
322        }
323    }
324
325    /// Clear search
326    pub fn clear_search(&mut self) {
327        self.search = None;
328    }
329
330    /// Enter filter mode
331    pub fn enter_filter_mode(&mut self) {
332        self.mode = ViewMode::Filtering;
333        self.filter_input.clear();
334    }
335
336    /// Enter search mode  
337    pub fn enter_search_mode(&mut self) {
338        self.mode = ViewMode::Searching;
339        self.search_input.clear();
340    }
341
342    /// Exit special modes back to normal
343    pub fn exit_special_mode(&mut self) {
344        self.mode = ViewMode::Normal;
345    }
346
347    /// Handle navigation keys in normal mode
348    pub fn handle_navigation(&mut self, key: KeyEvent) -> bool {
349        if self.mode != ViewMode::Normal {
350            return false;
351        }
352
353        match key.code {
354            KeyCode::Up => {
355                if self.selected_row > 0 {
356                    self.selected_row -= 1;
357                    self.ensure_row_visible(self.selected_row);
358                }
359                true
360            }
361            KeyCode::Down => {
362                if self.selected_row + 1 < self.visible_rows.len() {
363                    self.selected_row += 1;
364                    self.ensure_row_visible(self.selected_row);
365                }
366                true
367            }
368            KeyCode::Left => {
369                if self.selected_col > 0 {
370                    self.selected_col -= 1;
371                    self.ensure_column_visible(self.selected_col);
372                }
373                true
374            }
375            KeyCode::Right => {
376                if self.selected_col + 1 < self.table.column_count() {
377                    self.selected_col += 1;
378                    self.ensure_column_visible(self.selected_col);
379                }
380                true
381            }
382            KeyCode::PageUp => {
383                let jump = self.page_size.min(self.selected_row);
384                self.selected_row -= jump;
385                self.ensure_row_visible(self.selected_row);
386                true
387            }
388            KeyCode::PageDown => {
389                let jump = self
390                    .page_size
391                    .min(self.visible_rows.len() - self.selected_row - 1);
392                self.selected_row += jump;
393                self.ensure_row_visible(self.selected_row);
394                true
395            }
396            KeyCode::Home => {
397                self.selected_row = 0;
398                self.scroll_offset = 0;
399                true
400            }
401            KeyCode::End => {
402                if !self.visible_rows.is_empty() {
403                    self.selected_row = self.visible_rows.len() - 1;
404                    self.ensure_row_visible(self.selected_row);
405                }
406                true
407            }
408            _ => false,
409        }
410    }
411
412    /// Handle filter input
413    pub fn handle_filter_input(&mut self, key: KeyEvent) -> bool {
414        if self.mode != ViewMode::Filtering {
415            return false;
416        }
417
418        match key.code {
419            KeyCode::Char(c) => {
420                self.filter_input.insert_char(c);
421                true
422            }
423            KeyCode::Backspace => {
424                self.filter_input.delete_char();
425                true
426            }
427            KeyCode::Left => {
428                self.filter_input.move_cursor_left();
429                true
430            }
431            KeyCode::Right => {
432                self.filter_input.move_cursor_right();
433                true
434            }
435            KeyCode::Home => {
436                self.filter_input.move_cursor_home();
437                true
438            }
439            KeyCode::End => {
440                self.filter_input.move_cursor_end();
441                true
442            }
443            KeyCode::Enter => {
444                // Apply the filter
445                self.apply_filter(self.filter_input.text.clone(), None, false);
446                self.exit_special_mode();
447                true
448            }
449            KeyCode::Esc => {
450                self.exit_special_mode();
451                true
452            }
453            _ => false,
454        }
455    }
456
457    /// Handle search input
458    pub fn handle_search_input(&mut self, key: KeyEvent) -> bool {
459        if self.mode != ViewMode::Searching {
460            return false;
461        }
462
463        match key.code {
464            KeyCode::Char(c) => {
465                self.search_input.insert_char(c);
466                true
467            }
468            KeyCode::Backspace => {
469                self.search_input.delete_char();
470                true
471            }
472            KeyCode::Left => {
473                self.search_input.move_cursor_left();
474                true
475            }
476            KeyCode::Right => {
477                self.search_input.move_cursor_right();
478                true
479            }
480            KeyCode::Home => {
481                self.search_input.move_cursor_home();
482                true
483            }
484            KeyCode::End => {
485                self.search_input.move_cursor_end();
486                true
487            }
488            KeyCode::Enter => {
489                // Apply the search
490                self.start_search(self.search_input.text.clone(), false);
491                self.exit_special_mode();
492                true
493            }
494            KeyCode::Esc => {
495                self.exit_special_mode();
496                true
497            }
498            _ => false,
499        }
500    }
501
502    /// Get the currently selected cell value
503    pub fn get_selected_value(&self) -> Option<&DataValue> {
504        let visible_row = *self.visible_rows.get(self.selected_row)?;
505        self.table.get_value(visible_row, self.selected_col)
506    }
507
508    /// Get the currently selected column index
509    pub fn get_selected_column(&self) -> usize {
510        self.selected_col
511    }
512
513    /// Get status information for display
514    pub fn get_status_info(&self) -> String {
515        let total_rows = self.table.row_count();
516        let visible_rows = self.visible_rows.len();
517        let current_row = self.selected_row + 1;
518
519        let mut status = format!("Row {}/{}", current_row, visible_rows);
520
521        if visible_rows != total_rows {
522            status.push_str(&format!(" (filtered from {})", total_rows));
523        }
524
525        if let Some(ref filter) = self.filter {
526            status.push_str(&format!(" | Filter: '{}'", filter.pattern));
527        }
528
529        if let Some(ref search) = self.search {
530            status.push_str(&format!(
531                " | Search: '{}' ({} matches)",
532                search.pattern,
533                search.matches.len()
534            ));
535        }
536
537        if let Some(ref sort) = self.sort {
538            let col_name = &self.table.columns[sort.column_index].name;
539            let order = match sort.order {
540                SortOrder::Ascending => "↑",
541                SortOrder::Descending => "↓",
542            };
543            status.push_str(&format!(" | Sort: {} {}", col_name, order));
544        }
545
546        status
547    }
548
549    /// Create a ratatui Table widget for rendering
550    pub fn create_table_widget(&self) -> Table<'_> {
551        // Create header for visible columns only
552        let header = Row::new(
553            self.table.columns[self.visible_col_start..self.visible_col_end]
554                .iter()
555                .enumerate()
556                .map(|(i, col)| {
557                    let actual_col_idx = self.visible_col_start + i;
558                    let mut style = Style::default().add_modifier(Modifier::BOLD);
559                    if actual_col_idx == self.selected_col {
560                        style = style.bg(Color::Blue);
561                    }
562                    // Just show column name since we display all columns
563                    Cell::from(col.name.as_str()).style(style)
564                }),
565        )
566        .style(Style::default().bg(Color::DarkGray));
567
568        // Create visible rows
569        let start = self.scroll_offset;
570        let end = (start + self.page_size).min(self.visible_rows.len());
571
572        let rows: Vec<Row> = (start..end)
573            .map(|visible_idx| {
574                let row_idx = self.visible_rows[visible_idx];
575                let is_selected = visible_idx == self.selected_row;
576                let is_search_match = self.is_search_match(row_idx);
577
578                // Only show cells for visible columns
579                let cells: Vec<Cell> = (self.visible_col_start..self.visible_col_end)
580                    .map(|col_idx| {
581                        let value = self
582                            .table
583                            .get_value(row_idx, col_idx)
584                            .map(|v| v.to_string())
585                            .unwrap_or_else(|| "".to_string());
586
587                        let mut style = Style::default();
588
589                        if is_selected && col_idx == self.selected_col {
590                            style = style.bg(Color::Yellow).fg(Color::Black);
591                        } else if is_selected {
592                            style = style.bg(Color::Blue).fg(Color::White);
593                        } else if is_search_match && self.is_cell_search_match(row_idx, col_idx) {
594                            style = style.bg(Color::Green).fg(Color::Black);
595                        }
596
597                        Cell::from(value).style(style)
598                    })
599                    .collect();
600
601                Row::new(cells)
602            })
603            .collect();
604
605        // Calculate constraints based on visible column widths only
606        let constraints: Vec<Constraint> = self.column_widths
607            [self.visible_col_start..self.visible_col_end]
608            .iter()
609            .map(|&width| Constraint::Length(width))
610            .collect();
611
612        Table::new(rows, constraints)
613            .header(header)
614            .block(Block::default().borders(Borders::ALL).title("Data"))
615            .row_highlight_style(Style::default().bg(Color::Blue))
616    }
617
618    /// Create input widget for filter/search modes
619    pub fn create_input_widget(&self) -> Option<Paragraph<'_>> {
620        match self.mode {
621            ViewMode::Filtering => Some(
622                Paragraph::new(format!("Filter: {}", self.filter_input.text))
623                    .block(Block::default().borders(Borders::ALL).title("Filter")),
624            ),
625            ViewMode::Searching => Some(
626                Paragraph::new(format!("Search: {}", self.search_input.text))
627                    .block(Block::default().borders(Borders::ALL).title("Search")),
628            ),
629            _ => None,
630        }
631    }
632
633    // Private helper methods
634
635    fn update_visible_rows(&mut self) {
636        // Start with all rows
637        let mut visible: Vec<usize> = (0..self.table.row_count()).collect();
638
639        // Apply filter
640        if let Some(ref filter) = self.filter {
641            visible.retain(|&row_idx| self.matches_filter(row_idx, filter));
642        }
643
644        // Apply sort
645        if let Some(ref sort) = self.sort {
646            visible.sort_by(|&a, &b| self.compare_rows(a, b, sort));
647        }
648
649        self.visible_rows = visible;
650        self.column_widths = Self::calculate_column_widths(&self.table, &self.visible_rows);
651    }
652
653    fn matches_filter(&self, row_idx: usize, filter: &FilterConfig) -> bool {
654        let pattern = if filter.case_sensitive {
655            filter.pattern.clone()
656        } else {
657            filter.pattern.to_lowercase()
658        };
659
660        if let Some(col_idx) = filter.column_index {
661            // Filter specific column
662            if let Some(value) = self.table.get_value(row_idx, col_idx) {
663                let text = if filter.case_sensitive {
664                    value.to_string()
665                } else {
666                    value.to_string().to_lowercase()
667                };
668                text.contains(&pattern)
669            } else {
670                false
671            }
672        } else {
673            // Filter all columns
674            (0..self.table.column_count()).any(|col_idx| {
675                if let Some(value) = self.table.get_value(row_idx, col_idx) {
676                    let text = if filter.case_sensitive {
677                        value.to_string()
678                    } else {
679                        value.to_string().to_lowercase()
680                    };
681                    text.contains(&pattern)
682                } else {
683                    false
684                }
685            })
686        }
687    }
688
689    fn compare_rows(&self, a: usize, b: usize, sort: &SortConfig) -> std::cmp::Ordering {
690        use std::cmp::Ordering;
691
692        let val_a = self.table.get_value(a, sort.column_index);
693        let val_b = self.table.get_value(b, sort.column_index);
694
695        let result = match (val_a, val_b) {
696            (Some(a), Some(b)) => self.compare_values(a, b),
697            (Some(_), None) => Ordering::Less,
698            (None, Some(_)) => Ordering::Greater,
699            (None, None) => Ordering::Equal,
700        };
701
702        match sort.order {
703            SortOrder::Ascending => result,
704            SortOrder::Descending => result.reverse(),
705        }
706    }
707
708    fn compare_values(&self, a: &DataValue, b: &DataValue) -> std::cmp::Ordering {
709        use crate::datatable::DataValue;
710        use std::cmp::Ordering;
711
712        match (a, b) {
713            (DataValue::Integer(a), DataValue::Integer(b)) => a.cmp(b),
714            (DataValue::Float(a), DataValue::Float(b)) => {
715                a.partial_cmp(b).unwrap_or(Ordering::Equal)
716            }
717            (DataValue::String(a), DataValue::String(b)) => a.cmp(b),
718            (DataValue::Boolean(a), DataValue::Boolean(b)) => a.cmp(b),
719            (DataValue::DateTime(a), DataValue::DateTime(b)) => a.cmp(b),
720            (DataValue::Null, DataValue::Null) => Ordering::Equal,
721            (DataValue::Null, _) => Ordering::Greater,
722            (_, DataValue::Null) => Ordering::Less,
723            // Mixed types - convert to strings for comparison
724            (a, b) => a.to_string().cmp(&b.to_string()),
725        }
726    }
727
728    fn find_matches(&self, pattern: &str, case_sensitive: bool) -> Vec<(usize, usize)> {
729        let search_pattern = if case_sensitive {
730            pattern.to_string()
731        } else {
732            pattern.to_lowercase()
733        };
734
735        let mut matches = Vec::new();
736
737        for &row_idx in &self.visible_rows {
738            for col_idx in 0..self.table.column_count() {
739                if let Some(value) = self.table.get_value(row_idx, col_idx) {
740                    let text = if case_sensitive {
741                        value.to_string()
742                    } else {
743                        value.to_string().to_lowercase()
744                    };
745
746                    if text.contains(&search_pattern) {
747                        matches.push((row_idx, col_idx));
748                    }
749                }
750            }
751        }
752
753        matches
754    }
755
756    fn is_search_match(&self, row_idx: usize) -> bool {
757        if let Some(ref search) = self.search {
758            search.matches.iter().any(|(r, _)| *r == row_idx)
759        } else {
760            false
761        }
762    }
763
764    fn is_cell_search_match(&self, row_idx: usize, col_idx: usize) -> bool {
765        if let Some(ref search) = self.search {
766            search.matches.contains(&(row_idx, col_idx))
767        } else {
768            false
769        }
770    }
771
772    fn ensure_row_visible(&mut self, row_idx: usize) {
773        if row_idx < self.scroll_offset {
774            self.scroll_offset = row_idx;
775        } else if row_idx >= self.scroll_offset + self.page_size {
776            self.scroll_offset = row_idx - self.page_size + 1;
777        }
778    }
779
780    fn ensure_column_visible(&mut self, col_idx: usize) {
781        // If column is already visible, nothing to do
782        if col_idx >= self.visible_col_start && col_idx < self.visible_col_end {
783            return;
784        }
785
786        if col_idx < self.visible_col_start {
787            // Scrolling left - make this the first visible column
788            self.visible_col_start = col_idx;
789            // visible_col_end will be recalculated by update_viewport
790        } else if col_idx >= self.visible_col_end {
791            // Scrolling right - shift view to show this column
792            self.visible_col_start =
793                col_idx - (self.visible_col_end - self.visible_col_start - 1).min(col_idx);
794            // visible_col_end will be recalculated by update_viewport
795        }
796
797        self.horizontal_scroll = self.visible_col_start;
798    }
799
800    fn calculate_column_widths(table: &DataTable, visible_rows: &[usize]) -> Vec<u16> {
801        let mut widths = Vec::new();
802
803        // Match enhanced TUI's constants exactly
804        const MIN_WIDTH: u16 = 4; // Minimum column width (enhanced uses 4)
805        const MAX_WIDTH: u16 = 50; // Maximum column width (enhanced uses 50)
806        const PADDING: u16 = 2; // Padding (enhanced adds 2)
807        const MAX_ROWS_TO_CHECK: usize = 100; // Sample size (enhanced uses 100)
808
809        // Determine which rows to sample (like enhanced TUI)
810        let total_rows = if visible_rows.is_empty() {
811            table.row_count()
812        } else {
813            visible_rows.len()
814        };
815
816        let rows_to_check: Vec<usize> = if total_rows <= MAX_ROWS_TO_CHECK {
817            // Check all rows for small datasets
818            if visible_rows.is_empty() {
819                (0..total_rows).collect()
820            } else {
821                visible_rows.iter().take(total_rows).copied().collect()
822            }
823        } else {
824            // Sample evenly distributed rows for large datasets
825            let step = total_rows / MAX_ROWS_TO_CHECK;
826            (0..MAX_ROWS_TO_CHECK)
827                .map(|i| {
828                    let idx = (i * step).min(total_rows - 1);
829                    if visible_rows.is_empty() {
830                        idx
831                    } else {
832                        visible_rows[idx]
833                    }
834                })
835                .collect()
836        };
837
838        for (col_idx, column) in table.columns.iter().enumerate() {
839            // Start with header width
840            let mut max_width = column.name.len();
841
842            // Check only sampled rows for this column
843            for &row_idx in &rows_to_check {
844                if let Some(value) = table.get_value(row_idx, col_idx) {
845                    let display_len = value.to_string().len();
846                    max_width = max_width.max(display_len);
847                }
848            }
849
850            // Add padding and set reasonable limits (exactly like enhanced TUI)
851            let optimal_width = (max_width + PADDING as usize)
852                .max(MIN_WIDTH as usize)
853                .min(MAX_WIDTH as usize) as u16;
854
855            widths.push(optimal_width);
856        }
857
858        widths
859    }
860}
861
862#[cfg(test)]
863mod tests {
864    use super::*;
865    use crate::datatable::{DataColumn, DataRow, DataType, DataValue};
866    use crossterm::event::KeyModifiers;
867
868    fn create_test_table() -> DataTable {
869        let mut table = DataTable::new("test");
870
871        table.add_column(DataColumn::new("id").with_type(DataType::Integer));
872        table.add_column(DataColumn::new("name").with_type(DataType::String));
873        table.add_column(DataColumn::new("score").with_type(DataType::Float));
874
875        table
876            .add_row(DataRow::new(vec![
877                DataValue::Integer(1),
878                DataValue::String("Alice".to_string()),
879                DataValue::Float(95.5),
880            ]))
881            .unwrap();
882
883        table
884            .add_row(DataRow::new(vec![
885                DataValue::Integer(2),
886                DataValue::String("Bob".to_string()),
887                DataValue::Float(87.3),
888            ]))
889            .unwrap();
890
891        table
892            .add_row(DataRow::new(vec![
893                DataValue::Integer(3),
894                DataValue::String("Charlie".to_string()),
895                DataValue::Float(92.1),
896            ]))
897            .unwrap();
898
899        table
900    }
901
902    #[test]
903    fn test_datatable_view_creation() {
904        let table = create_test_table();
905        let view = DataTableView::new(table);
906
907        assert_eq!(view.visible_row_count(), 3);
908        assert_eq!(view.mode(), ViewMode::Normal);
909        assert!(view.filter.is_none());
910        assert!(view.search.is_none());
911        assert!(view.sort.is_none());
912    }
913
914    #[test]
915    fn test_filter() {
916        let table = create_test_table();
917        let mut view = DataTableView::new(table);
918
919        // Filter for names containing "li"
920        view.apply_filter("li".to_string(), None, false);
921
922        assert_eq!(view.visible_row_count(), 2); // Alice and Charlie
923        assert!(view.filter.is_some());
924    }
925
926    #[test]
927    fn test_sort() {
928        let table = create_test_table();
929        let mut view = DataTableView::new(table);
930
931        // Sort by score descending
932        view.apply_sort(2, SortOrder::Descending);
933
934        assert_eq!(view.visible_row_count(), 3);
935
936        // First visible row should have the highest score (Alice: 95.5)
937        let first_visible_row = view.visible_rows[0];
938        let first_value = view.table().get_value(first_visible_row, 1).unwrap();
939        assert_eq!(first_value.to_string(), "Alice");
940    }
941
942    #[test]
943    fn test_search() {
944        let table = create_test_table();
945        let mut view = DataTableView::new(table);
946
947        view.start_search("Bob".to_string(), false);
948
949        assert!(view.search.is_some());
950        let search = view.search.as_ref().unwrap();
951        assert_eq!(search.matches.len(), 1);
952        assert_eq!(search.current_match, Some((1, 1))); // Row 1, column 1 (name)
953    }
954
955    #[test]
956    fn test_navigation() {
957        let table = create_test_table();
958        let mut view = DataTableView::new(table);
959
960        assert_eq!(view.selected_row, 0);
961        assert_eq!(view.selected_col, 0);
962
963        // Move down
964        view.handle_navigation(KeyEvent::new(KeyCode::Down, KeyModifiers::NONE));
965        assert_eq!(view.selected_row, 1);
966
967        // Move right
968        view.handle_navigation(KeyEvent::new(KeyCode::Right, KeyModifiers::NONE));
969        assert_eq!(view.selected_col, 1);
970    }
971}