//! Board bring-up and peripheral wiring for the CMDR Joystick firmware.

use crate::button_matrix::{ButtonMatrix, MatrixPins};
use crate::hardware::{self, BoardPins};
use crate::status::StatusLed;
use cortex_m::delay::Delay;
use cortex_m::interrupt;
use eeprom24x::{addr_size, page_size, unique_serial, Eeprom24x};
use rp2040_hal::adc::{Adc, AdcPin};
use rp2040_hal::clocks::Clock;
use rp2040_hal::gpio::{self, Pin, PullNone};
use rp2040_hal::i2c::I2C;
use rp2040_hal::pac;
use rp2040_hal::pio::PIOExt;
use rp2040_hal::sio::Sio;
use rp2040_hal::timer::Timer;
use rp2040_hal::watchdog::Watchdog;
use rp2040_hal::{clocks::init_clocks_and_plls, gpio::FunctionSioInput};
use static_cell::StaticCell;
use usb_device::class_prelude::UsbBusAllocator;

pub type JoystickMatrix = ButtonMatrix<
    MatrixPins<{ hardware::BUTTON_ROWS }, { hardware::BUTTON_COLS }>,
    { hardware::BUTTON_ROWS },
    { hardware::BUTTON_COLS },
    { hardware::NUMBER_OF_BUTTONS },
>;

pub type JoystickStatusLed = StatusLed<pac::PIO0, rp2040_hal::pio::SM0, hardware::StatusLedPin>;

type BoardI2c = I2C<pac::I2C1, (hardware::I2cSdaPin, hardware::I2cSclPin)>;
type BoardEeprom = Eeprom24x<BoardI2c, page_size::B32, addr_size::TwoBytes, unique_serial::No>;

/// Strongly-typed collection of ADC-capable pins for each physical gimbal axis.
pub struct AxisAnalogPins {
    pub left_x: AdcPin<Pin<gpio::bank0::Gpio29, FunctionSioInput, PullNone>>,
    pub left_y: AdcPin<Pin<gpio::bank0::Gpio28, FunctionSioInput, PullNone>>,
    pub right_x: AdcPin<Pin<gpio::bank0::Gpio27, FunctionSioInput, PullNone>>,
    pub right_y: AdcPin<Pin<gpio::bank0::Gpio26, FunctionSioInput, PullNone>>,
}

impl AxisAnalogPins {
    fn new(inputs: hardware::AxisInputs) -> Self {
        // Wrap the raw GPIO inputs into ADC-capable pins for each physical axis.
        let left_x = AdcPin::new(inputs.left_x).unwrap();
        let left_y = AdcPin::new(inputs.left_y).unwrap();
        let right_x = AdcPin::new(inputs.right_x).unwrap();
        let right_y = AdcPin::new(inputs.right_y).unwrap();
        Self {
            left_x,
            left_y,
            right_x,
            right_y,
        }
    }
}

/// Aggregates the runtime peripherals used by the joystick firmware.
pub struct Board {
    button_matrix: JoystickMatrix,
    status_led: JoystickStatusLed,
    delay: Delay,
    timer: Timer,
    adc: Adc,
    axis_pins: AxisAnalogPins,
    left_extra_button: hardware::ExtraButtonPin,
    right_extra_button: hardware::ExtraButtonPin,
    eeprom: BoardEeprom,
    usb_bus: &'static UsbBusAllocator<rp2040_hal::usb::UsbBus>,
}

/// Board components handed off to the application after initialization.
pub struct BoardParts {
    pub button_matrix: JoystickMatrix,
    pub status_led: JoystickStatusLed,
    pub delay: Delay,
    pub timer: Timer,
    pub adc: Adc,
    pub axis_pins: AxisAnalogPins,
    pub left_extra_button: hardware::ExtraButtonPin,
    pub right_extra_button: hardware::ExtraButtonPin,
    pub eeprom: BoardEeprom,
    pub usb_bus: &'static UsbBusAllocator<rp2040_hal::usb::UsbBus>,
}

impl Board {
    pub fn new() -> Self {
        // Acquire RP2040 peripheral handles before configuration begins.
        let mut pac = pac::Peripherals::take().unwrap();
        let core = pac::CorePeripherals::take().unwrap();

        let mut watchdog = Watchdog::new(pac.WATCHDOG);
        // Bring up the primary system and USB clocks using the external crystal.
        let clocks = init_clocks_and_plls(
            hardware::XTAL_FREQ_HZ,
            pac.XOSC,
            pac.CLOCKS,
            pac.PLL_SYS,
            pac.PLL_USB,
            &mut pac.RESETS,
            &mut watchdog,
        )
        .ok()
        .unwrap();

        let sio = Sio::new(pac.SIO);
        // Split GPIO banks and translate them into the strongly typed board pins.
        let raw_pins = gpio::Pins::new(
            pac.IO_BANK0,
            pac.PADS_BANK0,
            sio.gpio_bank0,
            &mut pac.RESETS,
        );
        let pins = BoardPins::new(raw_pins);

        let matrix_pins = MatrixPins::new(pins.matrix_rows, pins.matrix_cols);
        // Create the button matrix scanner with firmware debounce parameters.
        let mut button_matrix = ButtonMatrix::new(
            matrix_pins,
            hardware::MATRIX_DEBOUNCE_SCANS,
            hardware::MIN_PRESS_SPACING_SCANS,
        );
        button_matrix.init_pins();

        let (mut pio, sm0, _, _, _) = pac.PIO0.split(&mut pac.RESETS);
        // Configure the WS2812 status LED using a dedicated PIO state machine.
        let status_led = StatusLed::new(
            pins.status_led,
            &mut pio,
            sm0,
            clocks.peripheral_clock.freq(),
        );

        // Set up timers for scheduling and a blocking delay helper.
        let timer = Timer::new(pac.TIMER, &mut pac.RESETS, &clocks);
        let delay = Delay::new(core.SYST, clocks.system_clock.freq().to_Hz());

        // Build the I²C bus and EEPROM driver used for calibration persistence.
        let i2c = I2C::i2c1(
            pac.I2C1,
            pins.i2c_sda,
            pins.i2c_scl,
            hardware::i2c::frequency(),
            &mut pac.RESETS,
            hardware::i2c::system_clock(),
        );
        let eeprom = Eeprom24x::new_24x32(i2c, hardware::i2c::EEPROM_ADDRESS);

        // Bring up the ADC block and wrap each axis input pin.
        let adc = Adc::new(pac.ADC, &mut pac.RESETS);
        let axis_pins = AxisAnalogPins::new(pins.axis_inputs);

        // Prepare a global USB bus allocator for the joystick HID device.
        let usb_bus = usb_allocator(
            pac.USBCTRL_REGS,
            pac.USBCTRL_DPRAM,
            clocks.usb_clock,
            &mut pac.RESETS,
        );

        Self {
            button_matrix,
            status_led,
            delay,
            timer,
            adc,
            axis_pins,
            left_extra_button: pins.left_extra_button,
            right_extra_button: pins.right_extra_button,
            eeprom,
            usb_bus,
        }
    }

    pub fn into_parts(self) -> BoardParts {
        BoardParts {
            button_matrix: self.button_matrix,
            status_led: self.status_led,
            delay: self.delay,
            timer: self.timer,
            adc: self.adc,
            axis_pins: self.axis_pins,
            left_extra_button: self.left_extra_button,
            right_extra_button: self.right_extra_button,
            eeprom: self.eeprom,
            usb_bus: self.usb_bus,
        }
    }
}

fn usb_allocator(
    usbctrl_regs: pac::USBCTRL_REGS,
    usbctrl_dpram: pac::USBCTRL_DPRAM,
    usb_clock: rp2040_hal::clocks::UsbClock,
    resets: &mut pac::RESETS,
) -> &'static UsbBusAllocator<rp2040_hal::usb::UsbBus> {
    // Lazily create the shared USB bus allocator so HID endpoints can borrow it.
    static USB_BUS: StaticCell<UsbBusAllocator<rp2040_hal::usb::UsbBus>> = StaticCell::new();

    // Wire up the USB bus allocator, HID class, and joystick endpoint once and reuse it.

    interrupt::free(|_| {
        USB_BUS.init_with(|| {
            UsbBusAllocator::new(rp2040_hal::usb::UsbBus::new(
                usbctrl_regs,
                usbctrl_dpram,
                usb_clock,
                true,
                resets,
            ))
        })
    })
}
