//! Button matrix scanner for CMDR Joystick.
//!
//! Mirrors the refactor performed for the keyboard firmware: the matrix owns
//! concrete pins, exposes a small `MatrixPinAccess` trait, and keeps the
//! debouncing + minimum press spacing behaviour identical to the original
//! joystick implementation.

use cortex_m::delay::Delay;
use embedded_hal::digital::{InputPin, OutputPin};
use rp2040_hal::gpio::{DynPinId, FunctionSioInput, FunctionSioOutput, Pin, PullNone, PullUp};

/// Abstraction over the matrix pins so the scanner can work with either the
/// concrete RP2040 pins or test doubles.
pub trait MatrixPinAccess<const ROWS: usize, const COLS: usize> {
    fn init_columns(&mut self);
    fn set_column_low(&mut self, column: usize);
    fn set_column_high(&mut self, column: usize);
    fn read_row(&mut self, row: usize) -> bool;
}

/// Concrete matrix pins backed by RP2040 GPIO using dynamic pin IDs.
type RowPin = Pin<DynPinId, FunctionSioInput, PullUp>;
type ColPin = Pin<DynPinId, FunctionSioOutput, PullNone>;

pub struct MatrixPins<const ROWS: usize, const COLS: usize> {
    rows: [RowPin; ROWS],
    cols: [ColPin; COLS],
}

impl<const ROWS: usize, const COLS: usize> MatrixPins<ROWS, COLS> {
    pub fn new(rows: [RowPin; ROWS], cols: [ColPin; COLS]) -> Self {
        Self { rows, cols }
    }
}

impl<const ROWS: usize, const COLS: usize> MatrixPinAccess<ROWS, COLS> for MatrixPins<ROWS, COLS> {
    fn init_columns(&mut self) {
        // Default all columns high so rows can be strobed one at a time.
        for column in self.cols.iter_mut() {
            let _ = column.set_high();
        }
    }

    fn set_column_low(&mut self, column: usize) {
        // Pull the active column low before scanning its rows.
        let _ = self.cols[column].set_low();
    }

    fn set_column_high(&mut self, column: usize) {
        // Release the column after scanning so other columns remain idle.
        let _ = self.cols[column].set_high();
    }

    fn read_row(&mut self, row: usize) -> bool {
        // Treat any low level as a pressed switch, defaulting to false on IO errors.
        self.rows[row].is_low().unwrap_or(false)
    }
}

/// Row/column scanned button matrix driver with debounce counters and minimum
/// spacing between subsequent presses of the same key.
pub struct ButtonMatrix<P, const ROWS: usize, const COLS: usize, const BUTTONS: usize>
where
    P: MatrixPinAccess<ROWS, COLS>,
{
    pins: P,
    pressed: [bool; BUTTONS],
    debounce_threshold: u8,
    debounce_counter: [u8; BUTTONS],
    last_press_scan: [u32; BUTTONS],
    min_press_gap_scans: u32,
    scan_counter: u32,
}

impl<P, const ROWS: usize, const COLS: usize, const BUTTONS: usize>
    ButtonMatrix<P, ROWS, COLS, BUTTONS>
where
    P: MatrixPinAccess<ROWS, COLS>,
{
    pub fn new(pins: P, debounce_threshold: u8, min_press_gap_scans: u32) -> Self {
        debug_assert_eq!(BUTTONS, ROWS * COLS);
        Self {
            pins,
            pressed: [false; BUTTONS],
            debounce_threshold,
            debounce_counter: [0; BUTTONS],
            last_press_scan: [0; BUTTONS],
            min_press_gap_scans,
            scan_counter: 0,
        }
    }

    pub fn init_pins(&mut self) {
        self.pins.init_columns();
    }

    pub fn prime(&mut self, delay: &mut Delay, passes: usize) {
        for _ in 0..passes {
            self.scan_matrix(delay);
        }
    }

    pub fn scan_matrix(&mut self, delay: &mut Delay) {
        self.scan_counter = self.scan_counter.wrapping_add(1);
        for column in 0..COLS {
            self.pins.set_column_low(column);
            delay.delay_us(1);
            self.process_column(column);
            self.pins.set_column_high(column);
            delay.delay_us(1);
        }
    }

    pub fn buttons_pressed(&self) -> [bool; BUTTONS] {
        self.pressed
    }

    fn process_column(&mut self, column: usize) {
        // Drive a single column scan to update button press history.
        for row in 0..ROWS {
            let index = column + (row * COLS);
            let current_state = self.pins.read_row(row);

            if current_state == self.pressed[index] {
                self.debounce_counter[index] = 0;
                continue;
            }

            self.debounce_counter[index] = self.debounce_counter[index].saturating_add(1);
            if self.debounce_counter[index] < self.debounce_threshold {
                continue;
            }

            self.debounce_counter[index] = 0;
            if current_state {
                if self.should_register_press(index) {
                    self.pressed[index] = true;
                }
            } else {
                self.pressed[index] = false;
            }
        }
    }

    fn should_register_press(&mut self, index: usize) -> bool {
        // Decide if a press should register given debounce timing.
        let elapsed = self.scan_counter.wrapping_sub(self.last_press_scan[index]);
        let can_register = self.last_press_scan[index] == 0 || elapsed >= self.min_press_gap_scans;

        if can_register {
            self.last_press_scan[index] = self.scan_counter;
        }

        can_register
    }

    #[cfg(all(test, feature = "std"))]
    pub(crate) fn process_column_for_test(&mut self, column: usize) {
        self.process_column(column);
    }

    #[cfg(all(test, feature = "std"))]
    pub(crate) fn set_scan_counter(&mut self, value: u32) {
        self.scan_counter = value;
    }

    #[cfg(all(test, feature = "std"))]
    pub(crate) fn bump_scan_counter(&mut self) {
        self.scan_counter = self.scan_counter.wrapping_add(1);
    }
}

#[cfg(all(test, feature = "std"))]
mod tests {
    use super::*;
    use core::cell::Cell;
    use std::rc::Rc;

    #[derive(Clone)]
    struct MockPins {
        row_state: Rc<Cell<bool>>,
        column_state: Rc<Cell<bool>>,
    }

    impl MockPins {
        fn new(row_state: Rc<Cell<bool>>, column_state: Rc<Cell<bool>>) -> Self {
            // Build a button matrix scanner with default state tracking arrays.
            Self {
                row_state,
                column_state,
            }
        }
    }

    impl MatrixPinAccess<1, 1> for MockPins {
        fn init_columns(&mut self) {
            // Simulate the hardware by driving the single column high by default.
            self.column_state.set(true);
        }

        fn set_column_low(&mut self, _column: usize) {
            // Drop the mock column low to emulate scanning behaviour.
            self.column_state.set(false);
        }

        fn set_column_high(&mut self, _column: usize) {
            // Release the mock column back to the idle high state.
            self.column_state.set(true);
        }

        fn read_row(&mut self, _row: usize) -> bool {
            // Return the mocked row state so tests can control pressed/unpressed.
            self.row_state.get()
        }
    }

    fn fixture() -> (
        ButtonMatrix<MockPins, 1, 1, 1>,
        Rc<Cell<bool>>,
        Rc<Cell<bool>>,
    ) {
        let row = Rc::new(Cell::new(false));
        let column = Rc::new(Cell::new(true));
        let pins = MockPins::new(row.clone(), column.clone());
        let matrix = ButtonMatrix::new(pins, 2, 3);
        (matrix, row, column)
    }

    #[test]
    fn debounce_requires_consecutive_scans() {
        // Debounce logic should require two consecutive pressed scans before registering.
        let (mut matrix, row, _column) = fixture();
        matrix.set_scan_counter(1);

        row.set(true);
        matrix.bump_scan_counter();
        matrix.process_column_for_test(0);
        assert!(!matrix.buttons_pressed()[0]);

        matrix.bump_scan_counter();
        matrix.process_column_for_test(0);
        assert!(matrix.buttons_pressed()[0]);
    }
}
