1use crate::buffer::{AppMode, Buffer, BufferAPI};
8use std::collections::VecDeque;
9use std::time::Instant;
10use tracing::{debug, info, warn};
11
12#[derive(Debug, Clone, PartialEq)]
14pub enum AppState {
15 Command,
17 Results,
19 Search { search_type: SearchType },
21 Help,
23 Debug,
25 History,
27 JumpToRow,
29 ColumnStats,
31}
32
33#[derive(Debug, Clone, PartialEq)]
35pub enum SearchType {
36 Vim, Column, Data, Fuzzy, }
41
42pub struct ShadowStateManager {
44 state: AppState,
46
47 previous_state: Option<AppState>,
49
50 history: VecDeque<StateTransition>,
52
53 transition_count: usize,
55
56 discrepancies: Vec<String>,
58}
59
60#[derive(Debug, Clone)]
61struct StateTransition {
62 timestamp: Instant,
63 from: AppState,
64 to: AppState,
65 trigger: String,
66}
67
68impl ShadowStateManager {
69 pub fn new() -> Self {
70 info!(target: "shadow_state", "Shadow state manager initialized");
71
72 Self {
73 state: AppState::Command,
74 previous_state: None,
75 history: VecDeque::with_capacity(100),
76 transition_count: 0,
77 discrepancies: Vec::new(),
78 }
79 }
80
81 pub fn observe_mode_change(&mut self, mode: AppMode, trigger: &str) {
83 let new_state = self.mode_to_state(mode.clone());
84
85 if new_state != self.state {
87 self.transition_count += 1;
88
89 info!(target: "shadow_state",
90 "[#{}] {} -> {} (trigger: {})",
91 self.transition_count,
92 self.state_display(&self.state),
93 self.state_display(&new_state),
94 trigger
95 );
96
97 let transition = StateTransition {
99 timestamp: Instant::now(),
100 from: self.state.clone(),
101 to: new_state.clone(),
102 trigger: trigger.to_string(),
103 };
104
105 self.history.push_back(transition);
106 if self.history.len() > 100 {
107 self.history.pop_front();
108 }
109
110 self.previous_state = Some(self.state.clone());
112 self.state = new_state;
113
114 self.log_expected_side_effects();
116 } else {
117 debug!(target: "shadow_state",
118 "Redundant mode change to {:?} ignored", mode);
119 }
120 }
121
122 pub fn set_mode(&mut self, mode: AppMode, buffer: &mut Buffer, trigger: &str) {
128 let new_state = self.mode_to_state(mode.clone());
129
130 if new_state != self.state {
132 self.transition_count += 1;
133
134 info!(target: "shadow_state",
135 "[#{}] {} -> {} (trigger: {})",
136 self.transition_count,
137 self.state_display(&self.state),
138 self.state_display(&new_state),
139 trigger
140 );
141
142 let transition = StateTransition {
144 timestamp: Instant::now(),
145 from: self.state.clone(),
146 to: new_state.clone(),
147 trigger: trigger.to_string(),
148 };
149
150 self.history.push_back(transition);
151 if self.history.len() > 100 {
152 self.history.pop_front();
153 }
154
155 self.previous_state = Some(self.state.clone());
157 self.state = new_state;
158
159 buffer.set_mode(mode);
161
162 self.log_expected_side_effects();
164 } else {
165 debug!(target: "shadow_state",
166 "Redundant mode change to {:?} ignored", mode);
167 }
168 }
169
170 pub fn switch_to_results(&mut self, buffer: &mut Buffer) {
172 self.set_mode(AppMode::Results, buffer, "switch_to_results");
173 }
174
175 pub fn switch_to_command(&mut self, buffer: &mut Buffer) {
177 self.set_mode(AppMode::Command, buffer, "switch_to_command");
178 }
179
180 pub fn start_search(&mut self, search_type: SearchType, buffer: &mut Buffer, trigger: &str) {
182 let mode = match search_type {
183 SearchType::Column => AppMode::ColumnSearch,
184 SearchType::Data | SearchType::Vim => AppMode::Search,
185 SearchType::Fuzzy => AppMode::FuzzyFilter,
186 };
187
188 self.state = AppState::Search {
190 search_type: search_type.clone(),
191 };
192 self.transition_count += 1;
193
194 info!(target: "shadow_state",
195 "[#{}] Starting {:?} search (trigger: {})",
196 self.transition_count, search_type, trigger
197 );
198
199 buffer.set_mode(mode);
201 }
202
203 pub fn exit_to_results(&mut self, buffer: &mut Buffer) {
205 self.set_mode(AppMode::Results, buffer, "exit_to_results");
206 }
207
208 pub fn observe_search_start(&mut self, search_type: SearchType, trigger: &str) {
212 let new_state = AppState::Search {
213 search_type: search_type.clone(),
214 };
215
216 if !matches!(self.state, AppState::Search { .. }) {
217 self.transition_count += 1;
218
219 info!(target: "shadow_state",
220 "[#{}] {} -> {:?} search (trigger: {})",
221 self.transition_count,
222 self.state_display(&self.state),
223 search_type,
224 trigger
225 );
226
227 self.previous_state = Some(self.state.clone());
228 self.state = new_state;
229
230 warn!(target: "shadow_state",
232 "⚠️ Search started - verify other search states were cleared!");
233 }
234 }
235
236 pub fn observe_search_end(&mut self, trigger: &str) {
238 if matches!(self.state, AppState::Search { .. }) {
239 let new_state = AppState::Results;
241
242 info!(target: "shadow_state",
243 "[#{}] Exiting search -> {} (trigger: {})",
244 self.transition_count,
245 self.state_display(&new_state),
246 trigger
247 );
248
249 self.previous_state = Some(self.state.clone());
250 self.state = new_state;
251
252 info!(target: "shadow_state",
254 "✓ Expected side effects: Clear search UI, restore navigation keys");
255 }
256 }
257
258 pub fn is_search_active(&self) -> bool {
260 matches!(self.state, AppState::Search { .. })
261 }
262
263 pub fn get_search_type(&self) -> Option<SearchType> {
265 if let AppState::Search { ref search_type } = self.state {
266 Some(search_type.clone())
267 } else {
268 None
269 }
270 }
271
272 pub fn status_display(&self) -> String {
274 format!("[Shadow: {}]", self.state_display(&self.state))
275 }
276
277 pub fn debug_info(&self) -> String {
279 let mut info = format!(
280 "Shadow State Debug (transitions: {})\n",
281 self.transition_count
282 );
283 info.push_str(&format!("Current: {:?}\n", self.state));
284
285 if !self.history.is_empty() {
286 info.push_str("\nRecent transitions:\n");
287 for transition in self.history.iter().rev().take(5) {
288 info.push_str(&format!(
289 " {:?} ago: {} -> {} ({})\n",
290 transition.timestamp.elapsed(),
291 self.state_display(&transition.from),
292 self.state_display(&transition.to),
293 transition.trigger
294 ));
295 }
296 }
297
298 if !self.discrepancies.is_empty() {
299 info.push_str("\n⚠️ Discrepancies detected:\n");
300 for disc in self.discrepancies.iter().rev().take(3) {
301 info.push_str(&format!(" - {}\n", disc));
302 }
303 }
304
305 info
306 }
307
308 pub fn report_discrepancy(&mut self, expected: &str, actual: &str) {
310 let msg = format!("Expected: {}, Actual: {}", expected, actual);
311 warn!(target: "shadow_state", "Discrepancy: {}", msg);
312 self.discrepancies.push(msg);
313 }
314
315 pub fn get_state(&self) -> &AppState {
321 &self.state
322 }
323
324 pub fn get_mode(&self) -> AppMode {
326 match &self.state {
327 AppState::Command => AppMode::Command,
328 AppState::Results => AppMode::Results,
329 AppState::Search { search_type } => match search_type {
330 SearchType::Column => AppMode::ColumnSearch,
331 SearchType::Data => AppMode::Search,
332 SearchType::Fuzzy => AppMode::FuzzyFilter,
333 SearchType::Vim => AppMode::Search, },
335 AppState::Help => AppMode::Help,
336 AppState::Debug => AppMode::Debug,
337 AppState::History => AppMode::History,
338 AppState::JumpToRow => AppMode::JumpToRow,
339 AppState::ColumnStats => AppMode::ColumnStats,
340 }
341 }
342
343 pub fn is_in_results_mode(&self) -> bool {
345 matches!(self.state, AppState::Results)
346 }
347
348 pub fn is_in_command_mode(&self) -> bool {
350 matches!(self.state, AppState::Command)
351 }
352
353 pub fn is_in_search_mode(&self) -> bool {
355 matches!(self.state, AppState::Search { .. })
356 }
357
358 pub fn is_in_help_mode(&self) -> bool {
360 matches!(self.state, AppState::Help)
361 }
362
363 pub fn is_in_debug_mode(&self) -> bool {
365 matches!(self.state, AppState::Debug)
366 }
367
368 pub fn is_in_history_mode(&self) -> bool {
370 matches!(self.state, AppState::History)
371 }
372
373 pub fn is_in_jump_mode(&self) -> bool {
375 matches!(self.state, AppState::JumpToRow)
376 }
377
378 pub fn is_in_column_stats_mode(&self) -> bool {
380 matches!(self.state, AppState::ColumnStats)
381 }
382
383 pub fn is_in_column_search(&self) -> bool {
385 matches!(
386 self.state,
387 AppState::Search {
388 search_type: SearchType::Column
389 }
390 )
391 }
392
393 pub fn is_in_data_search(&self) -> bool {
395 matches!(
396 self.state,
397 AppState::Search {
398 search_type: SearchType::Data
399 }
400 )
401 }
402
403 pub fn is_in_fuzzy_filter(&self) -> bool {
405 matches!(
406 self.state,
407 AppState::Search {
408 search_type: SearchType::Fuzzy
409 }
410 )
411 }
412
413 pub fn is_in_vim_search(&self) -> bool {
415 matches!(
416 self.state,
417 AppState::Search {
418 search_type: SearchType::Vim
419 }
420 )
421 }
422
423 pub fn get_previous_state(&self) -> Option<&AppState> {
425 self.previous_state.as_ref()
426 }
427
428 pub fn can_navigate(&self) -> bool {
430 self.is_in_results_mode()
431 }
432
433 pub fn can_edit(&self) -> bool {
435 self.is_in_command_mode() || self.is_in_search_mode()
436 }
437
438 pub fn get_transition_count(&self) -> usize {
440 self.transition_count
441 }
442
443 pub fn get_last_transition(&self) -> Option<&StateTransition> {
445 self.history.back()
446 }
447
448 fn mode_to_state(&self, mode: AppMode) -> AppState {
451 match mode {
452 AppMode::Command => AppState::Command,
453 AppMode::Results => AppState::Results,
454 AppMode::Search | AppMode::ColumnSearch => {
455 if let AppState::Search { ref search_type } = self.state {
457 AppState::Search {
458 search_type: search_type.clone(),
459 }
460 } else {
461 let search_type = match mode {
463 AppMode::ColumnSearch => SearchType::Column,
464 _ => SearchType::Data,
465 };
466 AppState::Search { search_type }
467 }
468 }
469 AppMode::Help => AppState::Help,
470 AppMode::Debug | AppMode::PrettyQuery => AppState::Debug,
471 AppMode::History => AppState::History,
472 AppMode::JumpToRow => AppState::JumpToRow,
473 AppMode::ColumnStats => AppState::ColumnStats,
474 _ => self.state.clone(), }
476 }
477
478 fn state_display(&self, state: &AppState) -> String {
479 match state {
480 AppState::Command => "COMMAND".to_string(),
481 AppState::Results => "RESULTS".to_string(),
482 AppState::Search { search_type } => format!("SEARCH({:?})", search_type),
483 AppState::Help => "HELP".to_string(),
484 AppState::Debug => "DEBUG".to_string(),
485 AppState::History => "HISTORY".to_string(),
486 AppState::JumpToRow => "JUMP_TO_ROW".to_string(),
487 AppState::ColumnStats => "COLUMN_STATS".to_string(),
488 }
489 }
490
491 fn log_expected_side_effects(&self) {
492 match (&self.previous_state, &self.state) {
493 (Some(AppState::Command), AppState::Results) => {
494 debug!(target: "shadow_state",
495 "Expected side effects: Clear searches, reset viewport, enable nav keys");
496 }
497 (Some(AppState::Results), AppState::Search { .. }) => {
498 debug!(target: "shadow_state",
499 "Expected side effects: Clear other searches, setup search UI");
500 }
501 (Some(AppState::Search { .. }), AppState::Results) => {
502 debug!(target: "shadow_state",
503 "Expected side effects: Clear search UI, restore nav keys");
504 }
505 _ => {}
506 }
507 }
508}
509
510impl Default for ShadowStateManager {
511 fn default() -> Self {
512 Self::new()
513 }
514}