/src/peripherals/usbdev.rs
use core::cell::RefCell;

use crate::bsp;
use crate::console::with_console;
use bsp::hal::clocks::UsbClock;
use bsp::hal::usb::UsbBus;
use bsp::pac::{interrupt, RESETS, USBCTRL_DPRAM, USBCTRL_REGS};
use critical_section::Mutex;
use serde::{ser::SerializeTuple, Serialize, Serializer};
use usb_device::{class_prelude::*, prelude::*};
use usbd_hid::descriptor::{gen_hid_descriptor, AsInputReport, SerializedDescriptor};
use usbd_hid::hid_class::{
    HIDClass, HidClassSettings, HidCountryCode, HidProtocol, HidSubClass, ProtocolModeConfig,
};
use usbd_serial::SerialPort;

#[gen_hid_descriptor(
    (collection = APPLICATION, usage_page = GENERIC_DESKTOP, usage = KEYBOARD) = {
        (usage_page = KEYBOARD, usage_min = 0xE0, usage_max = 0xE7) = {
            #[packed_bits 8]
            #[item_settings data, variable, absolute]
            modifier = input;
        };
        #[item_settings constant, array, absolute]
        reserved = input;
        (usage_page = LEDS, usage_min = 0x01, usage_max = 0x03) = {
            #[packed_bits 3]
            #[item_settings data, variable, absolute]
            leds = output;
        };
        (usage_page = KEYBOARD, usage_min = 0x00, usage_max = 0xFF) = {
            #[item_settings data, array, absolute]
            keycodes = input;
        };
    }
)]
struct KeyboardReport {
    pub modifier: u8,
    pub reserved: u8,
    pub leds: u8,
    pub keycodes: [u8; 6],
}

static mut USB_ALLOCATOR: Option<UsbBusAllocator<UsbBus>> = None;
static USB_MANAGER: Mutex<RefCell<Option<UsbManager>>> = Mutex::new(RefCell::new(None));

pub struct UsbManager<'a> {
    dev: UsbDevice<'a, UsbBus>,
    keyboard: HIDClass<'a, UsbBus>,
    serial: SerialPort<'a, UsbBus>,
}

impl<'a> UsbManager<'a> {
    pub fn update(&mut self) {
        if self.dev.poll(&mut [&mut self.keyboard, &mut self.serial]) {
            let mut buf = [0u8; 64];
            match self.serial.read(&mut buf) {
                Ok(n) => {
                    // n bytes read
                    with_console(|c| c.fill_read_buffer(&buf[0..n]));
                }
                Err(UsbError::WouldBlock) => (),
                Err(err) => panic!("Failed to read serial: {:?}", err),
            }
            match self.serial.flush() {
                Ok(_) => {
                    with_console(|c| c.write_ready());
                }
                Err(UsbError::WouldBlock) => (),
                Err(err) => panic!("Failed to flush serial: {:?}", err),
            }
        }
    }

    pub fn keyboard(&mut self) -> &mut HIDClass<'a, UsbBus> {
        &mut self.keyboard
    }

    pub fn serial(&mut self) -> &mut SerialPort<'a, UsbBus> {
        &mut self.serial
    }
}

pub fn init_usb(
    ctrl_reg: USBCTRL_REGS,
    ctrl_dpram: USBCTRL_DPRAM,
    pll: UsbClock,
    resets: &mut RESETS,
) {
    let bus = UsbBus::new(ctrl_reg, ctrl_dpram, pll, true, resets);

    // The Mutex<RefCell<...>> pattern doesn't work here because
    // the devices need to keep a reference to the allocator. So
    // we do something a little unsafe.
    let usballoc = unsafe {
        USB_ALLOCATOR = Some(UsbBusAllocator::new(bus));
        USB_ALLOCATOR.as_ref().unwrap()
    };

    let keyboard = HIDClass::new_ep_in_with_settings(
        usballoc,
        KeyboardReport::desc(),
        1,
        HidClassSettings {
            subclass: HidSubClass::NoSubClass,
            protocol: HidProtocol::Keyboard,
            config: ProtocolModeConfig::DefaultBehavior,
            locale: HidCountryCode::NotSupported,
        },
    );

    let serial = SerialPort::new(usballoc);

    let dev = UsbDeviceBuilder::new(usballoc, UsbVidPid(0x1209, 0x0001))
        .manufacturer("The Dominion of Awesome")
        .product("Smorgasboard")
        .serial_number("LOLBUTTS")
        .composite_with_iads()
        .build();

    critical_section::with(|cs| {
        let mut usb_manager = USB_MANAGER.borrow_ref_mut(cs);
        *usb_manager = Some(UsbManager {
            dev,
            keyboard,
            serial,
        })
    });
}

pub fn with_usb<F, R>(mut f: F) -> R
where
    F: FnMut(&mut UsbManager) -> R,
{
    critical_section::with(|cs| {
        let mut usb_manager = USB_MANAGER.borrow_ref_mut(cs);
        if let Some(ref mut usb_manager) = *usb_manager {
            return f(usb_manager);
        } else {
            panic!("USB Manager not initialized");
        }
    })
}

#[interrupt]
fn USBCTRL_IRQ() {
    with_usb(|u| u.update());
}