// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: MIT
//! Board support for ESP32-S3-LCD-EV-Board with display and touch controller support.

extern crate alloc;

// --- Slint platform integration imports ---
use slint::platform::software_renderer::Rgb565Pixel;
// --- FT5x06 Touch Controller ---
struct Ft5x06<I2C> {
    i2c: I2C,
    address: u8,
}

impl<I2C> Ft5x06<I2C>
where
    I2C: embedded_hal::i2c::I2c,
{
    pub fn new(i2c: I2C, address: u8) -> Self {
        Self { i2c, address }
    }

    /// Reads the first touch point. Returns Some((x, y)) if touched, None otherwise.
    pub fn get_touch(&mut self) -> Result<Option<(u16, u16)>, I2C::Error> {
        // 1) read touch count from register 0x02
        let mut buf = [0u8; 1];
        self.i2c.write_read(self.address, &[0x02], &mut buf)?;
        let count = buf[0] & 0x0F;
        if count == 0 {
            return Ok(None);
        }

        // 2) read first touch coordinates from regs 0x03..0x06
        let mut data = [0u8; 4];
        self.i2c.write_read(self.address, &[0x03], &mut data)?;
        let x = (((data[0] & 0x0F) as u16) << 8) | data[1] as u16;
        let y = (((data[2] & 0x0F) as u16) << 8) | data[3] as u16;

        Ok(Some((x, y)))
    }
}

use alloc::boxed::Box;
use esp_hal::dma::{CHUNK_SIZE, DmaDescriptor, DmaTxBuf};
use esp_hal::i2c;
use esp_hal::peripherals::Peripherals;
use slint::LogicalPosition;
use slint::PhysicalPosition;
use slint::PhysicalSize;

use alloc::rc::Rc;
use core::cell::RefCell;
use esp_hal::clock::CpuClock::_240MHz;
use esp_hal::delay::Delay;
use esp_hal::i2c::master::{Error, I2c};
use esp_hal::lcd_cam::{
    LcdCam,
    lcd::{
        ClockMode, Phase, Polarity,
        dpi::{Config as DpiConfig, Dpi, Format, FrameTiming},
    },
};
use esp_hal::time::Instant;
use esp_hal::{
    Blocking, Config as HalConfig,
    gpio::{Level, Output, OutputConfig},
    time::Rate,
};
use esp_println::logger::init_logger_from_env;
use i_slint_core::input::PointerEventButton;
use i_slint_core::platform::WindowEvent;
use log::{error, info};

// === Display constants ===
const LCD_H_RES: u16 = 480;
const LCD_V_RES: u16 = 480;
const FRAME_BYTES: usize = (LCD_H_RES as usize * LCD_V_RES as usize) * 2;
const NUM_DMA_DESC: usize = (FRAME_BYTES + CHUNK_SIZE - 1) / CHUNK_SIZE;

// Place DMA descriptors in DMA-capable RAM
#[link_section = ".dma"]
static mut TX_DESCRIPTORS: [DmaDescriptor; NUM_DMA_DESC] = [DmaDescriptor::EMPTY; NUM_DMA_DESC];

#[panic_handler]
fn panic(_info: &core::panic::PanicInfo) -> ! {
    error!("Panic: {}", _info);
    loop {}
}

struct EspBackend {
    window: RefCell<Option<Rc<slint::platform::software_renderer::MinimalSoftwareWindow>>>,
    peripherals: RefCell<Option<Peripherals>>,
}

impl Default for EspBackend {
    fn default() -> Self {
        EspBackend { window: RefCell::new(None), peripherals: RefCell::new(None) }
    }
}

/// Initialize the heap and set the Slint platform.
pub fn init() {
    // Initialize peripherals first.
    let peripherals = esp_hal::init(HalConfig::default().with_cpu_clock(_240MHz));
    init_logger_from_env();
    info!("Peripherals initialized");

    // Initialize the PSRAM allocator.
    esp_alloc::psram_allocator!(peripherals.PSRAM, esp_hal::psram);

    // Create and install the Slint backend that owns the peripherals.
    slint::platform::set_platform(Box::new(EspBackend {
        window: RefCell::new(None),
        peripherals: RefCell::new(Some(peripherals)),
    }))
    .expect("Slint platform already initialized");
}

impl slint::platform::Platform for EspBackend {
    fn create_window_adapter(
        &self,
    ) -> Result<Rc<dyn slint::platform::WindowAdapter>, slint::PlatformError> {
        let window = slint::platform::software_renderer::MinimalSoftwareWindow::new(
            slint::platform::software_renderer::RepaintBufferType::ReusedBuffer,
        );
        self.window.replace(Some(window.clone()));
        Ok(window)
    }

    fn duration_since_start(&self) -> core::time::Duration {
        core::time::Duration::from_millis(Instant::now().duration_since_epoch().as_millis())
    }

    fn run_event_loop(&self) -> Result<(), slint::PlatformError> {
        // Reinitialize peripherals, PSRAM, and logger
        let peripherals = self.peripherals.borrow_mut().take().expect("Peripherals already taken");

        // Setup I2C for the TCA9554 IO expander
        let i2c = I2c::new(
            peripherals.I2C0,
            i2c::master::Config::default().with_frequency(Rate::from_khz(400)),
        )
        .unwrap()
        .with_sda(peripherals.GPIO47)
        .with_scl(peripherals.GPIO48);

        // Initialize the IO expander for controlling the display
        let mut expander = Tca9554::new(i2c);
        expander.write_output_reg(0b1111_0011).unwrap();
        expander.write_direction_reg(0b1111_0001).unwrap();

        let delay = Delay::new();
        info!("Initializing display...");

        // Set up the write_byte function for sending commands to the display
        let mut write_byte = |b: u8, is_cmd: bool| {
            const SCS_BIT: u8 = 0b0000_0010;
            const SCL_BIT: u8 = 0b0000_0100;
            const SDA_BIT: u8 = 0b0000_1000;

            let mut output = 0b1111_0001 & !SCS_BIT;
            expander.write_output_reg(output).unwrap();

            for bit in core::iter::once(!is_cmd).chain((0..8).map(|i| (b >> i) & 0b1 != 0).rev()) {
                let prev = output;
                if bit {
                    output |= SDA_BIT;
                } else {
                    output &= !SDA_BIT;
                }
                if prev != output {
                    expander.write_output_reg(output).unwrap();
                }

                output &= !SCL_BIT;
                expander.write_output_reg(output).unwrap();

                output |= SCL_BIT;
                expander.write_output_reg(output).unwrap();
            }

            output &= !SCL_BIT;
            expander.write_output_reg(output).unwrap();

            output &= !SDA_BIT;
            expander.write_output_reg(output).unwrap();

            output |= SCS_BIT;
            expander.write_output_reg(output).unwrap();
        };

        // VSYNC must be high during initialization
        let mut vsync_pin = peripherals.GPIO3;
        let vsync_guard = Output::new(vsync_pin.reborrow(), Level::High, OutputConfig::default());

        // Initialize the display by sending the initialization commands
        for &init in INIT_CMDS.iter() {
            match init {
                InitCmd::Cmd(cmd, args) => {
                    write_byte(cmd, true);
                    for &arg in args {
                        write_byte(arg, false);
                    }
                }
                InitCmd::Delay(ms) => {
                    delay.delay_millis(ms as _);
                }
            }
        }
        drop(vsync_guard);

        // Set up DMA channel for LCD
        let tx_channel = peripherals.DMA_CH2;
        let lcd_cam = LcdCam::new(peripherals.LCD_CAM);

        // Configure the RGB display
        let config = DpiConfig::default()
            .with_clock_mode(ClockMode { polarity: Polarity::IdleLow, phase: Phase::ShiftLow })
            .with_frequency(Rate::from_mhz(10))
            .with_format(Format { enable_2byte_mode: true, ..Default::default() })
            .with_timing(FrameTiming {
                horizontal_active_width: LCD_H_RES as usize,
                vertical_active_height: LCD_V_RES as usize,
                horizontal_total_width: 600,
                horizontal_blank_front_porch: 80,
                vertical_total_height: 600,
                vertical_blank_front_porch: 80,
                hsync_width: 10,
                vsync_width: 4,
                hsync_position: 10,
            })
            .with_vsync_idle_level(Level::High)
            .with_hsync_idle_level(Level::High)
            .with_de_idle_level(Level::Low)
            .with_disable_black_region(false);

        let mut dpi = Dpi::new(lcd_cam.lcd, tx_channel, config)
            .unwrap()
            .with_vsync(vsync_pin.reborrow())
            .with_hsync(peripherals.GPIO46)
            .with_de(peripherals.GPIO17)
            .with_pclk(peripherals.GPIO9)
            .with_data0(peripherals.GPIO10)
            .with_data1(peripherals.GPIO11)
            .with_data2(peripherals.GPIO12)
            .with_data3(peripherals.GPIO13)
            .with_data4(peripherals.GPIO14)
            .with_data5(peripherals.GPIO21)
            .with_data6(peripherals.GPIO8)
            .with_data7(peripherals.GPIO18)
            .with_data8(peripherals.GPIO45)
            .with_data9(peripherals.GPIO38)
            .with_data10(peripherals.GPIO39)
            .with_data11(peripherals.GPIO40)
            .with_data12(peripherals.GPIO41)
            .with_data13(peripherals.GPIO42)
            .with_data14(peripherals.GPIO2)
            .with_data15(peripherals.GPIO1);

        info!("Display initialized, entering main loop...");

        const FRAME_PIXELS: usize = (LCD_H_RES as usize) * (LCD_V_RES as usize);
        const FRAME_BYTES: usize = FRAME_PIXELS * 2;

        // Allocate a PSRAM-backed DMA buffer for the frame
        let buf_box: Box<[u8; FRAME_BYTES]> = Box::new([0; FRAME_BYTES]);
        let psram_buf: &'static mut [u8] = Box::leak(buf_box);
        let mut dma_tx: DmaTxBuf = unsafe {
            let descriptors = &mut *core::ptr::addr_of_mut!(TX_DESCRIPTORS);
            DmaTxBuf::new(descriptors, psram_buf).unwrap()
        };
        let mut pixel_box: Box<[Rgb565Pixel; FRAME_PIXELS]> =
            Box::new([Rgb565Pixel(0); FRAME_PIXELS]);
        let pixel_buf: &mut [Rgb565Pixel] = &mut *pixel_box;
        // Initialize pixel buffer and DMA buffer
        // The pixel buffer will be filled by Slint's renderer in the main loop
        let dst = dma_tx.as_mut_slice();
        for (i, px) in pixel_buf.iter().enumerate() {
            let [lo, hi] = px.0.to_le_bytes();
            dst[2 * i] = lo;
            dst[2 * i + 1] = hi;
        }
        // Initial flush of the screen buffer
        match dpi.send(false, dma_tx) {
            Ok(xfer) => {
                let (_res, dpi2, tx2) = xfer.wait();
                dpi = dpi2;
                dma_tx = tx2;
            }
            Err((e, dpi2, tx2)) => {
                error!("Initial DMA send error: {:?}", e);
                dpi = dpi2;
                dma_tx = tx2;
            }
        }

        // Tell Slint the window dimensions match the DPI display resolution
        let size = PhysicalSize::new(LCD_H_RES.into(), LCD_V_RES.into());
        self.window.borrow().as_ref().expect("Window adapter not created").set_size(size);

        // Initialize FT5x06 touch controller on I2C1 (example pins)
        // Reclaim the I2C bus from the expander for FT5x06
        let i2c_bus = expander.into_i2c();
        let mut touch = Ft5x06::new(i2c_bus, 0x38);
        let mut last_touch: Option<LogicalPosition> = None;

        loop {
            // 1) Let Slint update its timers and animations
            slint::platform::update_timers_and_animations();

            if let Some(window) = self.window.borrow().clone() {
                window.request_redraw();
            }

            if let Some(window) = self.window.borrow().clone() {
                // Poll FT5x06 touch each frame since INT line is NC
                if let Ok(Some((x, y))) = touch.get_touch() {
                    let pos =
                        PhysicalPosition::new(x as i32, y as i32).to_logical(window.scale_factor());
                    if let Some(prev) = last_touch.replace(pos) {
                        if prev != pos {
                            window
                                .try_dispatch_event(WindowEvent::PointerMoved { position: pos })?;
                        }
                    } else {
                        window.try_dispatch_event(WindowEvent::PointerPressed {
                            position: pos,
                            button: PointerEventButton::Left,
                        })?;
                    }
                } else if let Some(pos) = last_touch.take() {
                    window.try_dispatch_event(WindowEvent::PointerReleased {
                        position: pos,
                        button: PointerEventButton::Left,
                    })?;
                    window.try_dispatch_event(WindowEvent::PointerExited)?;
                }

                // 2) Render the UI into Slint's software renderer buffer
                window.draw_if_needed(|renderer| {
                    let _dirty = renderer.render(pixel_buf, LCD_H_RES as usize);
                });

                // 3) Pack pixels into DMA buffer
                {
                    let dst = dma_tx.as_mut_slice();
                    for (i, px) in pixel_buf.iter().enumerate() {
                        let [lo, hi] = px.0.to_le_bytes();
                        dst[2 * i] = lo;
                        dst[2 * i + 1] = hi;
                    }
                }

                // 3) One-shot DMA transfer of the full frame
                match dpi.send(false, dma_tx) {
                    Ok(xfer) => {
                        let (res, dpi2, tx2) = xfer.wait();
                        dpi = dpi2;
                        dma_tx = tx2;
                        if let Err(e) = res {
                            error!("DMA error: {:?}", e);
                        }
                    }
                    Err((e, dpi2, tx2)) => {
                        error!("DMA send error: {:?}", e);
                        dpi = dpi2;
                        dma_tx = tx2;
                    }
                }

                // 4) If there are active animations, continue immediately
                if window.has_active_animations() {
                    continue;
                }
            }
        }
    }
}

// --- I2C expander (TCA9554) ---
struct Tca9554 {
    i2c: I2c<'static, esp_hal::Blocking>,
    address: u8,
}

impl Tca9554 {
    pub fn new(i2c: I2c<'static, esp_hal::Blocking>) -> Self {
        Self { i2c, address: 0x20 }
    }
    pub fn write_direction_reg(&mut self, value: u8) -> Result<(), Error> {
        self.i2c.write(self.address, &[0x03, value])
    }
    pub fn write_output_reg(&mut self, value: u8) -> Result<(), Error> {
        self.i2c.write(self.address, &[0x01, value])
    }

    pub fn into_i2c(self) -> I2c<'static, Blocking> {
        self.i2c
    }
}

// Display initialization commands for the ESP32-S3-LCD-EV-Board
#[derive(Copy, Clone, Debug)]
enum InitCmd {
    Cmd(u8, &'static [u8]),
    Delay(u8),
}

const INIT_CMDS: &[InitCmd] = &[
    InitCmd::Cmd(0xf0, &[0x55, 0xaa, 0x52, 0x08, 0x00]),
    InitCmd::Cmd(0xf6, &[0x5a, 0x87]),
    InitCmd::Cmd(0xc1, &[0x3f]),
    InitCmd::Cmd(0xc2, &[0x0e]),
    InitCmd::Cmd(0xc6, &[0xf8]),
    InitCmd::Cmd(0xc9, &[0x10]),
    InitCmd::Cmd(0xcd, &[0x25]),
    InitCmd::Cmd(0xf8, &[0x8a]),
    InitCmd::Cmd(0xac, &[0x45]),
    InitCmd::Cmd(0xa0, &[0xdd]),
    InitCmd::Cmd(0xa7, &[0x47]),
    InitCmd::Cmd(0xfa, &[0x00, 0x00, 0x00, 0x04]),
    InitCmd::Cmd(0x86, &[0x99, 0xa3, 0xa3, 0x51]),
    InitCmd::Cmd(0xa3, &[0xee]),
    InitCmd::Cmd(0xfd, &[0x3c, 0x3]),
    InitCmd::Cmd(0x71, &[0x48]),
    InitCmd::Cmd(0x72, &[0x48]),
    InitCmd::Cmd(0x73, &[0x00, 0x44]),
    InitCmd::Cmd(0x97, &[0xee]),
    InitCmd::Cmd(0x83, &[0x93]),
    InitCmd::Cmd(0x9a, &[0x72]),
    InitCmd::Cmd(0x9b, &[0x5a]),
    InitCmd::Cmd(0x82, &[0x2c, 0x2c]),
    InitCmd::Cmd(0xB1, &[0x10]),
    InitCmd::Cmd(
        0x6d,
        &[
            0x00, 0x1f, 0x19, 0x1a, 0x10, 0x0e, 0x0c, 0x0a, 0x02, 0x07, 0x1e, 0x1e, 0x1e, 0x1e,
            0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x08, 0x01, 0x09, 0x0b, 0x0d, 0x0f,
            0x1a, 0x19, 0x1f, 0x00,
        ],
    ),
    InitCmd::Cmd(
        0x64,
        &[
            0x38, 0x05, 0x01, 0xdb, 0x03, 0x03, 0x38, 0x04, 0x01, 0xdc, 0x03, 0x03, 0x7a, 0x7a,
            0x7a, 0x7a,
        ],
    ),
    InitCmd::Cmd(
        0x65,
        &[
            0x38, 0x03, 0x01, 0xdd, 0x03, 0x03, 0x38, 0x02, 0x01, 0xde, 0x03, 0x03, 0x7a, 0x7a,
            0x7a, 0x7a,
        ],
    ),
    InitCmd::Cmd(
        0x66,
        &[
            0x38, 0x01, 0x01, 0xdf, 0x03, 0x03, 0x38, 0x00, 0x01, 0xe0, 0x03, 0x03, 0x7a, 0x7a,
            0x7a, 0x7a,
        ],
    ),
    InitCmd::Cmd(
        0x67,
        &[
            0x30, 0x01, 0x01, 0xe1, 0x03, 0x03, 0x30, 0x02, 0x01, 0xe2, 0x03, 0x03, 0x7a, 0x7a,
            0x7a, 0x7a,
        ],
    ),
    InitCmd::Cmd(
        0x68,
        &[0x00, 0x08, 0x15, 0x08, 0x15, 0x7a, 0x7a, 0x08, 0x15, 0x08, 0x15, 0x7a, 0x7a],
    ),
    InitCmd::Cmd(0x60, &[0x38, 0x08, 0x7a, 0x7a, 0x38, 0x09, 0x7a, 0x7a]),
    InitCmd::Cmd(0x63, &[0x31, 0xe4, 0x7a, 0x7a, 0x31, 0xe5, 0x7a, 0x7a]),
    InitCmd::Cmd(0x69, &[0x04, 0x22, 0x14, 0x22, 0x14, 0x22, 0x08]),
    InitCmd::Cmd(0x6b, &[0x07]),
    InitCmd::Cmd(0x7a, &[0x08, 0x13]),
    InitCmd::Cmd(0x7b, &[0x08, 0x13]),
    InitCmd::Cmd(
        0xd1,
        &[
            0x00, 0x00, 0x00, 0x04, 0x00, 0x12, 0x00, 0x18, 0x00, 0x21, 0x00, 0x2a, 0x00, 0x35,
            0x00, 0x47, 0x00, 0x56, 0x00, 0x90, 0x00, 0xe5, 0x01, 0x68, 0x01, 0xd5, 0x01, 0xd7,
            0x02, 0x36, 0x02, 0xa6, 0x02, 0xee, 0x03, 0x48, 0x03, 0xa0, 0x03, 0xba, 0x03, 0xc5,
            0x03, 0xd0, 0x03, 0xe0, 0x03, 0xea, 0x03, 0xfa, 0x03, 0xff,
        ],
    ),
    InitCmd::Cmd(
        0xd2,
        &[
            0x00, 0x00, 0x00, 0x04, 0x00, 0x12, 0x00, 0x18, 0x00, 0x21, 0x00, 0x2a, 0x00, 0x35,
            0x00, 0x47, 0x00, 0x56, 0x00, 0x90, 0x00, 0xe5, 0x01, 0x68, 0x01, 0xd5, 0x01, 0xd7,
            0x02, 0x36, 0x02, 0xa6, 0x02, 0xee, 0x03, 0x48, 0x03, 0xa0, 0x03, 0xba, 0x03, 0xc5,
            0x03, 0xd0, 0x03, 0xe0, 0x03, 0xea, 0x03, 0xfa, 0x03, 0xff,
        ],
    ),
    InitCmd::Cmd(
        0xd3,
        &[
            0x00, 0x00, 0x00, 0x04, 0x00, 0x12, 0x00, 0x18, 0x00, 0x21, 0x00, 0x2a, 0x00, 0x35,
            0x00, 0x47, 0x00, 0x56, 0x00, 0x90, 0x00, 0xe5, 0x01, 0x68, 0x01, 0xd5, 0x01, 0xd7,
            0x02, 0x36, 0x02, 0xa6, 0x02, 0xee, 0x03, 0x48, 0x03, 0xa0, 0x03, 0xba, 0x03, 0xc5,
            0x03, 0xd0, 0x03, 0xe0, 0x03, 0xea, 0x03, 0xfa, 0x03, 0xff,
        ],
    ),
    InitCmd::Cmd(
        0xd4,
        &[
            0x00, 0x00, 0x00, 0x04, 0x00, 0x12, 0x00, 0x18, 0x00, 0x21, 0x00, 0x2a, 0x00, 0x35,
            0x00, 0x47, 0x00, 0x56, 0x00, 0x90, 0x00, 0xe5, 0x01, 0x68, 0x01, 0xd5, 0x01, 0xd7,
            0x02, 0x36, 0x02, 0xa6, 0x02, 0xee, 0x03, 0x48, 0x03, 0xa0, 0x03, 0xba, 0x03, 0xc5,
            0x03, 0xd0, 0x03, 0xe0, 0x03, 0xea, 0x03, 0xfa, 0x03, 0xff,
        ],
    ),
    InitCmd::Cmd(
        0xd5,
        &[
            0x00, 0x00, 0x00, 0x04, 0x00, 0x12, 0x00, 0x18, 0x00, 0x21, 0x00, 0x2a, 0x00, 0x35,
            0x00, 0x47, 0x00, 0x56, 0x00, 0x90, 0x00, 0xe5, 0x01, 0x68, 0x01, 0xd5, 0x01, 0xd7,
            0x02, 0x36, 0x02, 0xa6, 0x02, 0xee, 0x03, 0x48, 0x03, 0xa0, 0x03, 0xba, 0x03, 0xc5,
            0x03, 0xd0, 0x03, 0xe0, 0x03, 0xea, 0x03, 0xfa, 0x03, 0xff,
        ],
    ),
    InitCmd::Cmd(
        0xd6,
        &[
            0x00, 0x00, 0x00, 0x04, 0x00, 0x12, 0x00, 0x18, 0x00, 0x21, 0x00, 0x2a, 0x00, 0x35,
            0x00, 0x47, 0x00, 0x56, 0x00, 0x90, 0x00, 0xe5, 0x01, 0x68, 0x01, 0xd5, 0x01, 0xd7,
            0x02, 0x36, 0x02, 0xa6, 0x02, 0xee, 0x03, 0x48, 0x03, 0xa0, 0x03, 0xba, 0x03, 0xc5,
            0x03, 0xd0, 0x03, 0xe0, 0x03, 0xea, 0x03, 0xfa, 0x03, 0xff,
        ],
    ),
    InitCmd::Cmd(0x36, &[0x00]),
    InitCmd::Cmd(0x2A, &[0x00, 0x00, 0x01, 0xDF]), // 0 to 479 (0x1DF)
    // Set full row address range
    InitCmd::Cmd(0x2B, &[0x00, 0x00, 0x01, 0xDF]), // 0 to 479 (0x1DF)
    InitCmd::Cmd(0x3A, &[0x66]),
    InitCmd::Cmd(0x11, &[]),
    InitCmd::Delay(120),
    InitCmd::Cmd(0x29, &[]),
    InitCmd::Delay(20),
];
