//! TODO:
//! * GUI event dispatch.
//! * Async interrupt handling of keyboard input. Reduces LCD glitching.
//! * Attempt to reduce the size of the framebuffer to 240x960 by changing the front/back porch of
//!   the LCD driver. Reduces LCD glitching.
//! * Bounce buffers to get rid of glitching completely.
//!   https://esp32.com/viewtopic.php?t=28230
//!   https://esp32.com/viewtopic.php?f=12&t=26793&start=20#p95677
#![no_std]
#![no_main]
#![feature(macro_metavar_expr)]
#![feature(c_variadic)]
#![feature(c_size_t)]

extern crate alloc;

use core::alloc::Layout;
use core::cell::RefCell;
use core::ffi::CStr;

use alloc::string::ToString;
use alloc::boxed::Box;
use alloc::vec;
use bt_hci::controller::ExternalController;
use cfg_if::cfg_if;
use embassy_executor::Spawner;
use embassy_sync::blocking_mutex::raw::{CriticalSectionRawMutex};
use embassy_sync::channel::Channel;
use embassy_sync::signal::Signal;
use embassy_time::{Duration, Timer};
use esp_alloc::{HeapRegion, MemoryCapability};
use esp_hal::clock::CpuClock;
use esp_hal::dma::{BurstConfig, DmaDescriptor, DmaTxBuf, ExternalBurstConfig};
use esp_hal::gpio::{Flex, Input, InputConfig, Io, Level, Output, OutputConfig, Pull};
use esp_hal::handler;
use esp_hal::i2c::master::{I2c, I2cAddress};
use esp_hal::interrupt::software::SoftwareInterruptControl;
use esp_hal::lcd_cam::LcdCam;
use esp_hal::lcd_cam::lcd::dpi::Dpi;
use esp_hal::mcpwm::{McPwm, PeripheralClockConfig};
use esp_hal::psram::{FlashFreq, PsramConfig, PsramSize, SpiRamFreq, SpiTimingConfigCoreClock};
use esp_hal::rng::TrngSource;
use esp_hal::system::Stack;
use esp_hal::timer::timg::TimerGroup;
use esp_hal::{Blocking, ram};
use esp_radio::Controller as RadioController;
use esp_radio::ble::controller::BleConnector;
use esp_rtos::embassy::Executor;
use esp_storage::FlashStorage;
use log::{LevelFilter, error, info, warn};
use rmk::channel::{CONTROLLER_CHANNEL, ControllerSub};
use rmk::config::{BehaviorConfig, PositionalConfig, RmkConfig, StorageConfig, VialConfig};
use rmk::controller::{Controller, EventController};
use rmk::debounce::default_debouncer::DefaultDebouncer;
use rmk::embassy_futures::yield_now;
use rmk::event::{ControllerEvent, KeyboardEvent};
use rmk::hid::Report;
use rmk::input_device::Runnable;
use rmk::join_all;
use rmk::keyboard::Keyboard;
use rmk::storage::async_flash_wrapper;
use rmk::types::action::{Action, KeyAction};
use rmk::types::keycode::KeyCode;
use rmk::{initialize_keymap_and_storage, run_devices, run_rmk};
use slint::platform::software_renderer::Rgb565Pixel;
use slint::ComponentHandle;
use static_cell::StaticCell;
use ui::AppWindow;
use xkbcommon::xkb::{self, Keymap};
use {esp_alloc as _, esp_backtrace as _};

use crate::matrix::IoeMatrix;
use crate::peripherals::st7701s::St7701s;
use crate::ui::backend::{FramebufferPtr, SlintBackend};
use crate::vial::{VIAL_KEYBOARD_DEF, VIAL_KEYBOARD_ID, CustomKeycodes};

mutually_exclusive_features::none_or_one_of!["usb-log", "alt-log", "rtt-log"];

mod keymap;
mod matrix;
mod peripherals;
mod vial;
mod ui;
mod logging;
mod ffi;

#[cfg(feature = "alt-log")]
mod console;

// This creates a default app-descriptor required by the esp-idf bootloader.
// For more information see: <https://docs.espressif.com/projects/esp-idf/en/stable/esp32/api-reference/system/app_image_format.html#application-description>
esp_bootloader_esp_idf::esp_app_desc!();

cfg_if! {
    if #[cfg(feature = "trace")] {
        const LOG_LEVEL_FILTER: LevelFilter = LevelFilter::Trace;
    } else if #[cfg(feature = "debug")] {
        const LOG_LEVEL_FILTER: LevelFilter = LevelFilter::Debug;
    } else if #[cfg(feature = "info")] {
        const LOG_LEVEL_FILTER: LevelFilter = LevelFilter::Info;
    } else if #[cfg(feature = "warn")] {
        const LOG_LEVEL_FILTER: LevelFilter = LevelFilter::Warn;
    } else if #[cfg(feature = "error")] {
        const LOG_LEVEL_FILTER: LevelFilter = LevelFilter::Error;
    } else {
        const LOG_LEVEL_FILTER: LevelFilter = LevelFilter::Off;
    }
}

const FRAME_DURATION_MIN: Duration = Duration::from_millis(40); // 25 FPS

static PSRAM_ALLOCATOR: esp_alloc::EspHeap = esp_alloc::EspHeap::empty();

/// Used to signal that MCU is ready to submit the framebuffer to the LCD.
static SIGNAL_LCD_SUBMIT: Signal<CriticalSectionRawMutex, ()> = Signal::new();

/// Used to signal that the MCU is ready to render the GUI.
static SIGNAL_UI_RENDER: Signal<CriticalSectionRawMutex, ()> = Signal::new();

#[esp_rtos::main]
async fn main(_spawner: Spawner) {
    #[cfg(feature = "usb-log")]
    {
        esp_println::logger::init_logger(LOG_LEVEL_FILTER);
        info!("Logger initialized!");
    }

    #[cfg(feature = "rtt-log")]
    rtt_target::rtt_init_log!(LOG_LEVEL_FILTER);

    let config = esp_hal::Config::default()
        .with_cpu_clock(CpuClock::max())
        .with_psram(PsramConfig {
            size: PsramSize::AutoDetect,
            core_clock: Some(SpiTimingConfigCoreClock::SpiTimingConfigCoreClock80m),
            flash_frequency: FlashFreq::default(),
            ram_frequency: SpiRamFreq::Freq80m,
        });
    let peripherals: esp_hal::peripherals::Peripherals = esp_hal::init(config);
    info!("System initialized!");

    #[cfg(feature = "alt-log")]
    let alt_uart_rx_task = {
        use esp_hal::uart::Uart;

        let (uart_rx, uart_tx) = Uart::new(peripherals.UART2, Default::default()).unwrap().with_tx(peripherals.GPIO12).with_rx(peripherals.GPIO5).split();
        logging::setup_alternative_logging(uart_tx, LOG_LEVEL_FILTER);
        info!("Logger initialized!");
        console::run_console(uart_rx.into_async())
    };

    #[cfg(not(feature = "alt-log"))]
    let alt_uart_rx_task = async {};

    // Use the internal DRAM as the heap.
    esp_alloc::heap_allocator!(#[unsafe(link_section = ".dram2_uninit")] size: 64 * 1024);
    info!("Heap initialized! {:#?}", esp_alloc::HEAP.stats());

    // Initialize the PSRAM allocator.
    {
        let (psram_offset, psram_size) = esp_hal::psram::psram_raw_parts(&peripherals.PSRAM);
        unsafe {
            PSRAM_ALLOCATOR.add_region(HeapRegion::new(
                psram_offset,
                psram_size,
                MemoryCapability::External.into(),
            ));
        }
    }

    info!("PSRAM allocator initialized!");

    // let mut io = Io::new(peripherals.IO_MUX);
    // io.set_interrupt_handler(interrupt_handler);

    // info!("IO Mux initialized!");

    // Enable pull-up on GPIO0 to prevent booting into download mode.
    let gpio0 = Output::new(
        peripherals.GPIO0,
        Level::High,
        OutputConfig::default().with_pull(Pull::Up),
    );

    // Enable LDO2
    let _ = Output::new(peripherals.GPIO17, Level::High, OutputConfig::default());

    // Enable antenna
    let _ = Output::new(peripherals.GPIO11, Level::Low, OutputConfig::default());

    // TODO: Use PWM to control the pwm_pin.
    let mut _pwm = McPwm::new(peripherals.MCPWM0, PeripheralClockConfig::with_prescaler(1));
    let mut _pwm_pin = Output::new(peripherals.GPIO21, Level::High, OutputConfig::default());

    let timg0 = TimerGroup::new(peripherals.TIMG0);
    let software_interrupt = SoftwareInterruptControl::new(peripherals.SW_INTERRUPT);
    esp_rtos::start(
        timg0.timer0, /*, software_interrupt.software_interrupt0 */
    );

    info!("ESP-RTOS started!");

    #[cfg(feature = "ble")]
    let mut host_resources = rmk::HostResources::new();
    #[cfg(feature = "ble")]
    let stack = {
        // Enable the TRNG source, so `Trng` can be constructed.
        let _trng_source = TrngSource::new(peripherals.RNG, peripherals.ADC1);
        let mut rng = esp_hal::rng::Trng::try_new().unwrap();
        static RADIO: StaticCell<RadioController<'static>> = StaticCell::new();
        let radio = RADIO.init(esp_radio::init().unwrap());
        let bluetooth = peripherals.BT;
        let connector = BleConnector::new(radio, bluetooth, Default::default()).unwrap();
        let controller: ExternalController<_, 20> = ExternalController::new(connector);
        let central_addr = [0x18, 0xe2, 0x21, 0x80, 0xc0, 0xc7];
        let ble_stack = rmk::ble::build_ble_stack(controller, central_addr, &mut rng, &mut host_resources).await;

        info!("BLE stack for RMK built!");

        ble_stack
    };

    // Initialize USB
    #[cfg(not(feature = "no-usb"))]
    let usb_driver = {
        use core::ptr::addr_of_mut;
        use esp_hal::otg_fs::Usb;
        use esp_hal::otg_fs::asynch::{Config, Driver};

        static mut EP_MEMORY: [u8; 1024] = [0; 1024];
        let usb = Usb::new(peripherals.USB0, peripherals.GPIO20, peripherals.GPIO19);
        // Create the driver, from the HAL.
        let config = Config::default();
        let driver = Driver::new(usb, unsafe { &mut *addr_of_mut!(EP_MEMORY) }, config);

        info!("USB driver for RMK built!");

        driver
    };

    // Initialize the flash
    let flash = FlashStorage::new(peripherals.FLASH)
        // Flash memory may not be written to while another core is executing from it.
        // By default, `FlashStorage` is configured to abort the operation and log an error message.
        // However, it can also be configured to auto-park the other core, such that writing to
        // flash succeeds.
        // Alternatively, XiP from PSRAM could be used along with the `multicore_ignore` strategy,
        // to avoid having to park the other core, which could result in better performance.
        // Invalid configuration would then present itself as freezing/UB.
        .multicore_auto_park();
    let flash = async_flash_wrapper(flash);

    info!("Flash memory configured!");

    let sck = Output::new(peripherals.GPIO36, Level::High, OutputConfig::default());
    let mosi = Flex::new(peripherals.GPIO35);
    let cs = Output::new(peripherals.GPIO6, Level::High, OutputConfig::default());

    let lcd = LcdCam::new(peripherals.LCD_CAM).lcd;
    let unconfigured_dpi = Dpi::new(lcd, peripherals.DMA_CH2, Default::default())
        .unwrap()
        .with_de(peripherals.GPIO37)
        .with_pclk(peripherals.GPIO34)
        .with_hsync(peripherals.GPIO44)
        .with_vsync(peripherals.GPIO43)
        // Blue
        .with_data0(peripherals.GPIO38)
        .with_data1(peripherals.GPIO39)
        .with_data2(peripherals.GPIO40)
        .with_data3(peripherals.GPIO41)
        .with_data4(peripherals.GPIO42)
        // Green
        .with_data7(peripherals.GPIO13)
        .with_data8(peripherals.GPIO14)
        .with_data9(peripherals.GPIO15)
        .with_data10(peripherals.GPIO16)
        // Red
        .with_data11(gpio0)
        .with_data12(peripherals.GPIO1)
        .with_data13(peripherals.GPIO2)
        .with_data14(peripherals.GPIO3)
        .with_data15(peripherals.GPIO4);

    #[cfg(not(feature = "alt-log"))]
    let unconfigured_dpi = unconfigured_dpi
        // Green
        .with_data5(peripherals.GPIO5)
        .with_data6(peripherals.GPIO12);

    let st7701s = St7701s::new(sck, mosi, cs, unconfigured_dpi).await;

    info!("ST7701S-based LCD display initialized!");

    // RMK config
    let vial_config = VialConfig::new(VIAL_KEYBOARD_ID, VIAL_KEYBOARD_DEF, &[(0, 0), (1, 1)]);
    let storage_config = StorageConfig {
        start_addr: 0x3f0000,
        num_sectors: 16,
        ..Default::default()
    };
    let rmk_config = RmkConfig {
        vial_config,
        storage_config,
        ..Default::default()
    };

    // Initialze keyboard stuffs
    // Initialize the storage and keymap
    let mut default_keymap = keymap::get_default_keymap();
    let mut behavior_config = BehaviorConfig::default();
    let mut per_key_config = PositionalConfig::default();
    let (keymap, mut storage) = initialize_keymap_and_storage(
        &mut default_keymap,
        flash,
        &storage_config,
        &mut behavior_config,
        &mut per_key_config,
    )
    .await;

    info!("Initialized keymap and storage for RMK!");

    // Initialize the matrix and keyboard
    const I2C_ADDR_MATRIX_LEFT: I2cAddress = I2cAddress::SevenBit(0b0100000);
    const I2C_ADDR_MATRIX_RIGHT: I2cAddress = I2cAddress::SevenBit(0b0100001);

    let i2c = I2c::new(peripherals.I2C0, Default::default())
        .unwrap()
        .with_sda(peripherals.GPIO8)
        .with_scl(peripherals.GPIO9);

    let matrix_interrupt_low = Input::new(peripherals.GPIO7, InputConfig::default());

    let mut matrix = IoeMatrix::new(
        matrix_interrupt_low,
        i2c.into_async(),
        DefaultDebouncer::new(),
        [I2C_ADDR_MATRIX_LEFT, I2C_ADDR_MATRIX_RIGHT],
    )
    .await;
    let mut keyboard = Keyboard::new(&keymap); // Initialize the light controller

    info!("Keyboard initialized!");

    static FRAMEBUFFER: StaticCell<Framebuffer> = StaticCell::new();
    let framebuffer = FRAMEBUFFER.init(Framebuffer::new(
        360 + /* TODO: Figure out why more bytes are needed: */ 8,
        960,
    ));

    info!("Framebuffer created!");

    // let window_size = [framebuffer.width, framebuffer.height];
    let window_size = [framebuffer.height, framebuffer.width];
    let framebuffer_ptr = FramebufferPtr(framebuffer.as_target_pixels() as _);

    static SECOND_CORE_STACK: StaticCell<Stack<{8192 * 2}>> = StaticCell::new();
    let second_core_stack = SECOND_CORE_STACK.init(Stack::new());
    esp_rtos::start_second_core(
        peripherals.CPU_CTRL,
        software_interrupt.software_interrupt0,
        software_interrupt.software_interrupt1,
        second_core_stack,
        move || {
            // static EXECUTOR: StaticCell<InterruptExecutor<2>> = StaticCell::new();
            // let exec = EXECUTOR.init(InterruptExecutor::new(
            //     software_interrupt.software_interrupt2,
            // ));
            // let spawner = exec.start(Priority::Priority3);
            // spawner.must_spawn(run_renderer_task());
            static EXECUTOR: StaticCell<Executor> = StaticCell::new();
            let executor: &mut Executor = EXECUTOR.init(Executor::new());
            executor.run(|spawner| {
                let slint_backend = SlintBackend {
                    // peripherals: RefCell::new(Some(peripherals)),
                    window_size,
                    window: RefCell::new(None),
                    framebuffer: framebuffer_ptr,
                };
                spawner.must_spawn(run_renderer_task(slint_backend));
            });
        },
    );

    info!("Second core started!");

    let hid_report_proxy_task = {
        static KEYBOARD_REPORT_PROXY: Channel<CriticalSectionRawMutex, Report, 16> = Channel::new();

        {
            *rmk::channel::KEYBOARD_REPORT_SENDER.write().await = &KEYBOARD_REPORT_PROXY;
        }

        info!("HID Report Proxy Task: Waiting 1 sec...");
        Timer::after_secs(1).await;
        info!("HID Report Proxy Task: Done waiting.");
        const KEYMAP_STRING: &str = include_str!("../keymaps/cz.xkb");
        info!("HID Report Proxy Task: test");
        let context = xkb::Context::new(xkb::CONTEXT_NO_FLAGS);
        info!("HID Report Proxy Task: foo");
        let keymap = Keymap::new_from_string(&context, KEYMAP_STRING, xkb::KEYMAP_FORMAT_TEXT_V1, xkb::KEYMAP_COMPILE_NO_FLAGS).unwrap();
        info!("HID Report Proxy Task: bar");

        async {
            loop {
                let report = KEYBOARD_REPORT_PROXY.receive().await;

                info!("Intercepted HID report: {report:?}");
                rmk::channel::KEYBOARD_REPORT_RECEIVER.send(report).await;
            }
        }
    };

    let mut user_controller = UserController::new();

    info!("Awaiting on all tasks...");

    // TODO: Probably want to select! instead and re-try.
    join_all![
        // We currently send the framebuffer data using the main core, which does not seem to slow
        // down the rest of the tasks too much.
        run_lcd(st7701s, framebuffer),
        run_devices! (
            (matrix) => rmk::channel::EVENT_CHANNEL,
        ),
        keyboard.run(), // Keyboard is special
        run_rmk(
            &keymap,
            #[cfg(not(feature = "no-usb"))]
            usb_driver,
            #[cfg(feature = "ble")]
            &stack,
            &mut storage,
            rmk_config,
        ),
        hid_report_proxy_task,
        user_controller.event_loop(),
        alt_uart_rx_task
    ]
    .await;
}

struct UserController {
    sub: ControllerSub,
}

impl UserController {
    fn new() -> Self {
        Self {
            sub: CONTROLLER_CHANNEL.subscriber().unwrap(),
        }
    }
}

impl Controller for UserController {
    type Event = ControllerEvent;

    async fn process_event(&mut self, event: Self::Event) {
        if let ControllerEvent::Key(keyboard_event, KeyAction::Single(Action::Key(keycode))) = event && (KeyCode::Kb0..=KeyCode::User31).contains(&keycode) {
            warn!("{keycode:?} pressed.");

            if keycode as u16 == CustomKeycodes::FOCUS_LCD as u16 {
                warn!("Focus LCD pressed.");
            }
        }
    }

    async fn next_message(&mut self) -> Self::Event {
        self.sub.next_message_pure().await
    }
}

#[embassy_executor::task]
async fn run_renderer_task(backend: SlintBackend) {
    slint::platform::set_platform(Box::new(backend))
    .expect("backend already initialized");

    let main = AppWindow::new().unwrap();

    // Instead of having a `loop` in the non-async `SlintBackend::run_event_loop`, we achieve
    // async by having only one iteration of the loop run, and `await`ing here.
    // The following block is analogous to `main.run()`.
    {
        main.show().unwrap();

        loop {
            slint::run_event_loop().unwrap();
            SIGNAL_LCD_SUBMIT.signal(());
            #[cfg(feature = "limit-fps")]
            embassy_time::Timer::after(FRAME_DURATION_MIN).await;
            SIGNAL_UI_RENDER.wait().await;
        }

        #[expect(unreachable_code)]
        main.hide().unwrap();
    }
}

struct Framebuffer {
    width: u32,
    height: u32,
    dma_buf: Option<DmaTxBuf>,
}

impl Framebuffer {
    pub fn new(width: u32, height: u32) -> Self {
        let buffer_len = width as usize * height as usize * core::mem::size_of::<u16>();
        // Allocate the framebuffer in the external PSRAM memory.
        // Note: We just leak this buffer.
        let buffer_ptr = unsafe {
            // ⚠️ Note: For chips that support DMA to/from PSRAM (ESP32-S3) DMA transfers to/from PSRAM
            // have extra alignment requirements. The address and size of the buffer pointed to by each
            // descriptor must be a multiple of the cache line (block) size. This is 32 bytes on ESP32-S3.
            PSRAM_ALLOCATOR.alloc_caps(
                MemoryCapability::External.into(),
                Layout::from_size_align(buffer_len, 32).unwrap(),
            )
        };
        let buffer = unsafe { core::slice::from_raw_parts_mut(buffer_ptr, buffer_len) };
        let burst_config: BurstConfig = ExternalBurstConfig::Size16.into();

        info!(
            "PSRAM SPI burst config: max_compatible_chunk_size={}",
            burst_config.max_compatible_chunk_size()
        );
        let dma_buf_descs_len = esp_hal::dma::descriptor_count(
            buffer_len,
            burst_config.max_compatible_chunk_size(),
            false,
        );
        // Descriptors are initialized by `DmaTxBuf::new`.
        let dma_buf_descs =
            Box::leak(vec![DmaDescriptor::EMPTY; dma_buf_descs_len].into_boxed_slice());
        let dma_buf = DmaTxBuf::new(dma_buf_descs, buffer).unwrap();

        Self {
            width,
            height,
            dma_buf: Some(dma_buf),
        }
    }

    pub fn as_target_pixels(&mut self) -> &mut [Rgb565Pixel] {
        bytemuck::cast_slice_mut::<_, Rgb565Pixel>(self.dma_buf.as_mut().unwrap().as_mut_slice())
    }
}

#[embassy_executor::task]
async fn run_lcd_task(st7701s: St7701s<'static, Blocking>, framebuffer: &'static mut Framebuffer) {
    run_lcd(st7701s, framebuffer).await
}

async fn run_lcd(mut st7701s: St7701s<'static, Blocking>, framebuffer: &'static mut Framebuffer) {
    loop {
        // Timer::after(Duration::from_millis(100)).await;
        // yield_now().await;
        SIGNAL_LCD_SUBMIT.wait().await;

        // TODO: Use bounce buffers:
        // https://docs.espressif.com/projects/esp-idf/en/v5.0/esp32s3/api-reference/peripherals/lcd.html#bounce-buffer-with-single-psram-frame-buffer
        // They need to be implemented in esp-hal.
        let transfer = match st7701s.dpi.send(false, framebuffer.dma_buf.take().unwrap()) {
            Err((error, result_dpi, result_dma_buf)) => {
                error!(
                    "An error occurred while initiating transfer of the framebuffer to the LCD display: {error:?}"
                );
                st7701s.dpi = result_dpi;
                framebuffer.dma_buf = Some(result_dma_buf);
                continue;
            }
            Ok(transfer) => transfer,
        };

        // This could be used to allow other tasks to be executed on the first core, but that causes
        // the flash to be accessed, which interferes with the framebuffer transfer.
        // For that reason, it is disabled, and this task blocks the first core, until the transfer
        // is complete.
        #[cfg(not(feature = "limit-fps"))]
        while !transfer.is_done() {
            // Timer::after_millis(1).await;
            yield_now().await;
        }

        let result;
        let dma_buf;
        (result, st7701s.dpi, dma_buf) = transfer.wait();
        framebuffer.dma_buf = Some(dma_buf);

        SIGNAL_UI_RENDER.signal(());

        if let Err(error) = result {
            error!(
                "An error occurred while transferring framebuffer to the LCD display: {error:?}"
            );
        }
    }
}

// // TODO: Not needed currently. If it is ever enabled, don't forget to register it in Io.
// #[handler]
// #[ram] // TODO: Is this necessary?
// fn interrupt_handler() {
//     // esp_println::println!(
//     //     "GPIO Interrupt with priority {}",
//     //     esp_hal::xtensa_lx::interrupt::get_level()
//     // );
// }
