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