//! Keyboard input dispatch. Pure function from key event → `Action`
//! so the keymap is unit-testable without spinning up a TUI.

use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};

/// Pan step in cells per keypress. Small enough to stay smooth, large
/// enough that hjkl feels responsive on big canvases.
pub const PAN_STEP: i32 = 5;
/// Geometric zoom factor. 1.25× per `+` keeps zooming snappy without
/// jumping over content.
pub const ZOOM_FACTOR: f32 = 1.25;

#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Action {
    None,
    Quit,
    Pan { dx: i32, dy: i32 },
    Zoom { factor: f32 },
    ToggleHelp,
    CloseHelp,
}

/// Map a key press to an [`Action`]. `help_open` lets `Esc` and `q`
/// close the overlay instead of quitting when it's visible.
pub fn dispatch(key: KeyEvent, help_open: bool) -> Action {
    if key.modifiers.contains(KeyModifiers::CONTROL) && key.code == KeyCode::Char('c') {
        return Action::Quit;
    }
    match key.code {
        KeyCode::Char('q') => {
            if help_open {
                Action::CloseHelp
            } else {
                Action::Quit
            }
        }
        KeyCode::Esc => {
            if help_open {
                Action::CloseHelp
            } else {
                Action::Quit
            }
        }
        KeyCode::Char('?') => Action::ToggleHelp,
        KeyCode::Char('h') | KeyCode::Left => Action::Pan {
            dx: -PAN_STEP,
            dy: 0,
        },
        KeyCode::Char('l') | KeyCode::Right => Action::Pan {
            dx: PAN_STEP,
            dy: 0,
        },
        KeyCode::Char('k') | KeyCode::Up => Action::Pan {
            dx: 0,
            dy: -PAN_STEP,
        },
        KeyCode::Char('j') | KeyCode::Down => Action::Pan {
            dx: 0,
            dy: PAN_STEP,
        },
        KeyCode::Char('+') | KeyCode::Char('=') => Action::Zoom {
            factor: ZOOM_FACTOR,
        },
        KeyCode::Char('-') | KeyCode::Char('_') => Action::Zoom {
            factor: 1.0 / ZOOM_FACTOR,
        },
        _ => Action::None,
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crossterm::event::{KeyCode, KeyEvent, KeyEventKind, KeyEventState, KeyModifiers};

    fn k(code: KeyCode) -> KeyEvent {
        KeyEvent {
            code,
            modifiers: KeyModifiers::NONE,
            kind: KeyEventKind::Press,
            state: KeyEventState::NONE,
        }
    }

    #[test]
    fn hjkl_pans() {
        assert_eq!(
            dispatch(k(KeyCode::Char('h')), false),
            Action::Pan {
                dx: -PAN_STEP,
                dy: 0
            }
        );
        assert_eq!(
            dispatch(k(KeyCode::Char('l')), false),
            Action::Pan {
                dx: PAN_STEP,
                dy: 0
            }
        );
        assert_eq!(
            dispatch(k(KeyCode::Char('j')), false),
            Action::Pan {
                dx: 0,
                dy: PAN_STEP
            }
        );
        assert_eq!(
            dispatch(k(KeyCode::Char('k')), false),
            Action::Pan {
                dx: 0,
                dy: -PAN_STEP
            }
        );
    }

    #[test]
    fn arrows_pan() {
        assert_eq!(
            dispatch(k(KeyCode::Right), false),
            Action::Pan {
                dx: PAN_STEP,
                dy: 0,
            }
        );
        assert_eq!(
            dispatch(k(KeyCode::Up), false),
            Action::Pan {
                dx: 0,
                dy: -PAN_STEP,
            }
        );
    }

    #[test]
    fn plus_minus_zoom() {
        match dispatch(k(KeyCode::Char('+')), false) {
            Action::Zoom { factor } => assert!((factor - ZOOM_FACTOR).abs() < f32::EPSILON),
            other => panic!("got {other:?}"),
        }
        match dispatch(k(KeyCode::Char('-')), false) {
            Action::Zoom { factor } => assert!((factor - 1.0 / ZOOM_FACTOR).abs() < f32::EPSILON),
            other => panic!("got {other:?}"),
        }
    }

    #[test]
    fn q_quits_when_help_closed_closes_when_open() {
        assert_eq!(dispatch(k(KeyCode::Char('q')), false), Action::Quit);
        assert_eq!(dispatch(k(KeyCode::Char('q')), true), Action::CloseHelp);
    }

    #[test]
    fn ctrl_c_always_quits() {
        let key = KeyEvent {
            code: KeyCode::Char('c'),
            modifiers: KeyModifiers::CONTROL,
            kind: KeyEventKind::Press,
            state: KeyEventState::NONE,
        };
        assert_eq!(dispatch(key, false), Action::Quit);
        assert_eq!(dispatch(key, true), Action::Quit);
    }

    #[test]
    fn question_toggles_help() {
        assert_eq!(dispatch(k(KeyCode::Char('?')), false), Action::ToggleHelp);
        assert_eq!(dispatch(k(KeyCode::Char('?')), true), Action::ToggleHelp);
    }
}
