use alloc::{alloc::Global, boxed::Box, vec::Vec};
use embassy_time::Timer;
use esp_hal::{
    Blocking,
    dma::{BurstConfig, ExternalBurstConfig, InternalBurstConfig},
    gpio::{Flex, Level, Output, OutputConfig},
    lcd_cam::{LcdCam, lcd::dpi::Dpi},
    ledc::{self, LSGlobalClkSource, Ledc, LowSpeed},
    spi::master::AnySpi,
};
use esp_hal_bounce_buffers::{DmaBounce, Swapchain, SwapchainWriter, allocate_dma_buffer_in};
use i_slint_core::software_renderer::{Rgb565Pixel, TargetPixel};
use log::{error, info};

use crate::{DmaBounceController, PSRAM_ALLOCATOR, peripherals::st7701s::St7701s};

#[allow(non_snake_case)]
pub struct DisplayPeripherals {
    pub DMA_CH2: esp_hal::peripherals::DMA_CH2<'static>,
    pub LCD_CAM: esp_hal::peripherals::LCD_CAM<'static>,
    pub LEDC: esp_hal::peripherals::LEDC<'static>,
    pub GPIO0: Output<'static>,
    pub GPIO1: esp_hal::peripherals::GPIO1<'static>,
    pub GPIO2: esp_hal::peripherals::GPIO2<'static>,
    pub GPIO3: esp_hal::peripherals::GPIO3<'static>,
    pub GPIO4: esp_hal::peripherals::GPIO4<'static>,
    #[cfg(not(feature = "alt-log"))]
    pub GPIO5: esp_hal::peripherals::GPIO5<'static>,
    pub GPIO6: esp_hal::peripherals::GPIO6<'static>,
    #[cfg(not(feature = "alt-log"))]
    pub GPIO12: esp_hal::peripherals::GPIO12<'static>,
    pub GPIO13: esp_hal::peripherals::GPIO13<'static>,
    pub GPIO14: esp_hal::peripherals::GPIO14<'static>,
    pub GPIO15: esp_hal::peripherals::GPIO15<'static>,
    pub GPIO16: esp_hal::peripherals::GPIO16<'static>,
    pub GPIO21: esp_hal::peripherals::GPIO21<'static>,
    pub GPIO34: esp_hal::peripherals::GPIO34<'static>,
    pub GPIO35: esp_hal::peripherals::GPIO35<'static>,
    pub GPIO36: esp_hal::peripherals::GPIO36<'static>,
    pub GPIO37: esp_hal::peripherals::GPIO37<'static>,
    pub GPIO38: esp_hal::peripherals::GPIO38<'static>,
    pub GPIO39: esp_hal::peripherals::GPIO39<'static>,
    pub GPIO40: esp_hal::peripherals::GPIO40<'static>,
    pub GPIO41: esp_hal::peripherals::GPIO41<'static>,
    pub GPIO42: esp_hal::peripherals::GPIO42<'static>,
    pub GPIO43: esp_hal::peripherals::GPIO43<'static>,
    pub GPIO44: esp_hal::peripherals::GPIO44<'static>,
}

impl DisplayPeripherals {
    pub async fn into_display(self) -> St7701s<'static, Blocking> {
        let mut ledc = Ledc::new(self.LEDC);
        ledc.set_global_slow_clock(LSGlobalClkSource::APBClk);
        let bl_timer = ledc.timer::<LowSpeed>(ledc::timer::Number::Timer0);
        let bl_channel = ledc.channel::<LowSpeed>(ledc::channel::Number::Channel0, self.GPIO21);

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

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

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

        let st7701s = St7701s::new(sck, mosi, cs, unconfigured_dpi, bl_timer, bl_channel).await;
        info!("ST7701S-based LCD display initialized!");
        st7701s
    }
}

// TODO: Rename or get rid of.
pub struct Framebuffer {
    pub width: u32,
    pub height: u32,
    pub swapchain: Option<SwapchainWriter>,
    pub bounce_buffers: Option<DmaBounce>,
}

impl Framebuffer {
    pub fn new(
        channel: esp_hal::peripherals::DMA_CH0<'static>,
        peripheral_src: AnySpi<'static>,
        peripheral_dst: Dpi<'static, Blocking>,
        burst_config: BurstConfig,
        front_porch_pixels: u32,
        width_pixels: u32,
        height_pixels: u32,
        rows_per_window: usize,
        cyclic: bool,
    ) -> Self {
        const BYTES_PER_PIXEL: usize = core::mem::size_of::<u16>();
        let buffer_size = width_pixels as usize * height_pixels as usize * BYTES_PER_PIXEL;
        let framebuffers = [
            Box::leak(allocate_dma_buffer_in(
                buffer_size,
                burst_config,
                &PSRAM_ALLOCATOR,
            )),
            Box::leak(allocate_dma_buffer_in(
                buffer_size,
                burst_config,
                &PSRAM_ALLOCATOR,
            )),
        ];
        let (swapchain_reader, swapchain_writer) = Swapchain { framebuffers }.into_reader_writer();
        let bounce_buffers = DmaBounce::new(
            Global,
            channel,
            peripheral_src,
            peripheral_dst,
            swapchain_reader,
            front_porch_pixels as usize * BYTES_PER_PIXEL,
            width_pixels as usize * BYTES_PER_PIXEL,
            rows_per_window,
            burst_config,
            cyclic,
        );

        Self {
            width: width_pixels,
            height: height_pixels,
            swapchain: Some(swapchain_writer),
            bounce_buffers: Some(bounce_buffers),
        }
    }
}

#[allow(unused)]
pub async fn test_bounce_buffers(
    channel: esp_hal::peripherals::DMA_CH0<'static>,
    peripheral: esp_hal::peripherals::SPI2<'static>,
    display_peripherals: DisplayPeripherals,
) {
    error!("TEST BOUNCE BUFFERS SECTION ENTERED");

    const BYTES_PER_PIXEL: usize = core::mem::size_of::<u16>();
    // Assume highest burst config setting.
    const EXTERNAL_BURST_CONFIG: ExternalBurstConfig = ExternalBurstConfig::Size32;
    const ALIGNMENT_PIXELS: usize = EXTERNAL_BURST_CONFIG as usize / BYTES_PER_PIXEL;
    // The total number of pixels demanded by the DPI, per row.
    const WIDTH_TOTAL_PIXELS: usize = 368;
    // The total number of rows demanded by the DPI, per frame.
    const HEIGHT_PIXELS: usize = 960;
    // The number of unused pixels at the start of the row.
    const FRONT_PORCH_ACTUAL_PIXELS: usize = 120;
    // The number of actually visible pixels, per row.
    const WIDTH_VISIBLE_PIXELS: usize = 240;
    // The number of pixels not stored in a bounce buffer, per row.
    // This many arbitrary pixels are sent to the DPI.
    const FRONT_PORCH_SKIPPED_PIXELS: usize =
        (FRONT_PORCH_ACTUAL_PIXELS / ALIGNMENT_PIXELS) * ALIGNMENT_PIXELS;
    const WIDTH_STORED_PIXELS: usize = WIDTH_TOTAL_PIXELS - FRONT_PORCH_SKIPPED_PIXELS;
    const VISIBLE_OFFSET_IN_BUFFER_PIXELS: usize =
        FRONT_PORCH_ACTUAL_PIXELS - FRONT_PORCH_SKIPPED_PIXELS;
    const ROWS_PER_WINDOW: usize = 16;
    let burst_config = BurstConfig {
        internal_memory: InternalBurstConfig::Enabled,
        external_memory: EXTERNAL_BURST_CONFIG,
    };
    let (swapchain_reader, mut swapchain_writer) = Swapchain {
        framebuffers: [
            Box::leak(allocate_dma_buffer_in(
                HEIGHT_PIXELS * WIDTH_STORED_PIXELS * BYTES_PER_PIXEL,
                burst_config,
                &PSRAM_ALLOCATOR,
            )),
            Box::leak(allocate_dma_buffer_in(
                HEIGHT_PIXELS * WIDTH_STORED_PIXELS * BYTES_PER_PIXEL,
                burst_config,
                &PSRAM_ALLOCATOR,
            )),
        ],
    }
    .into_reader_writer();

    {
        let write_guard = &mut swapchain_writer.write();
        let buffer_src = bytemuck::cast_slice_mut::<u8, Rgb565Pixel>(write_guard);
        let colors = (0..WIDTH_VISIBLE_PIXELS as u8 / 2)
            .rev()
            .map(|val| Rgb565Pixel::from_rgb(0xFF, val * 2, 0))
            .collect::<Vec<_>>();
        for (index, pixel) in buffer_src.iter_mut().enumerate() {
            let mut x =
                (index % WIDTH_STORED_PIXELS) as i16 - VISIBLE_OFFSET_IN_BUFFER_PIXELS as i16;
            let mut y = (index / WIDTH_STORED_PIXELS) as i16;

            if x < WIDTH_VISIBLE_PIXELS as i16 {
                x = core::cmp::min(x, WIDTH_VISIBLE_PIXELS as i16 - 1 - x);
                y = core::cmp::min(y, HEIGHT_PIXELS as i16 - 1 - y);
                let min = core::cmp::min(x, y);

                *pixel = colors[min as usize % colors.len()];
                continue;
            }

            *pixel = Rgb565Pixel::default();
        }
    }

    let mut st7701s = display_peripherals.into_display().await;
    let mut dma_bounce = DmaBounce::new(
        Global,
        channel,
        AnySpi::from(peripheral),
        st7701s.dpi,
        swapchain_reader,
        FRONT_PORCH_SKIPPED_PIXELS * BYTES_PER_PIXEL,
        WIDTH_STORED_PIXELS * BYTES_PER_PIXEL,
        ROWS_PER_WINDOW,
        burst_config,
        true,
    );
    let mut bb_controller = DmaBounceController::new(dma_bounce);

    error!("TEST BOUNCE BUFFERS TASK LAUNCHED");

    loop {
        bb_controller.start().await.unwrap();
        st7701s.controller.sleep_off().await;
        Timer::after_secs(1).await;
        st7701s.controller.sleep_on().await;
        bb_controller.stop().await.unwrap();
        Timer::after_secs(1).await;
    }
}
