// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: MIT

use alloc::boxed::Box;
use alloc::rc::Rc;
use core::cell::RefCell;
use embedded_hal::delay::DelayNs;
use embedded_hal_bus::i2c::RefCellDevice;
use esp_alloc as _;
use esp_backtrace as _;
use esp_hal::clock::CpuClock;
use esp_hal::delay::Delay;
use esp_hal::dma::{DmaRxBuf, DmaTxBuf};
use esp_hal::dma_buffers;
use esp_hal::i2c::master::{Config as I2cConfig, I2c};
use esp_hal::spi::Mode;
use esp_hal::spi::master::{Config as SpiConfig, Spi};
use esp_hal::time::{Instant, Rate};
use esp_println::logger::init_logger_from_env;
use ft3x68_rs::{FT3168_DEVICE_ADDRESS, Ft3x68Driver, ResetInterface};
use log::info;
use sh8601_rs::{
    ColorMode, DMA_CHUNK_SIZE, DisplaySize, ResetDriver, Sh8601Driver, Ws18AmoledDriver,
    framebuffer_size,
};
use slint::platform::{PointerEventButton, WindowEvent};
use slint::{LogicalPosition, PhysicalPosition, Rgb8Pixel};
use static_cell::StaticCell;

// Provide a reset implementation for the FT3x68 touch driver
// In the Waveshare 1.8" AMOLED display, the reset pin is controlled via an I2C GPIO expander (TCA9554PWR).
// The touch reset pin is connected to Pin 2
pub struct TouchResetDriver<I2C> {
    i2c: I2C,
}

impl<I2C> TouchResetDriver<I2C> {
    pub fn new(i2c: I2C) -> Self {
        TouchResetDriver { i2c }
    }
}

impl<I2C> ResetInterface for TouchResetDriver<I2C>
where
    I2C: embedded_hal::i2c::I2c,
{
    type Error = I2C::Error;

    fn reset(&mut self) -> Result<(), Self::Error> {
        let delay = Delay::new();
        self.i2c.write(0x20, &[0x03, 0x00])?; // Configure all pins as output
        self.i2c.write(0x20, &[0x01, 0b0000_0000])?; // Drive low
        delay.delay_millis(20);
        self.i2c.write(0x20, &[0x01, 0b0000_0100])?; // Drive high
        delay.delay_millis(300);
        Ok(())
    }
}

// Display configuration for Waveshare ESP32-S3-Touch-AMOLED-1.8
const DISPLAY_SIZE: DisplaySize = DisplaySize::new(368, 448);
const FB_SIZE: usize = framebuffer_size(DISPLAY_SIZE, ColorMode::Rgb888);
const FRAME_PIXELS: usize = 368 * 448;

type SharedI2c = RefCellDevice<'static, I2c<'static, esp_hal::Blocking>>;
type BoardDisplay =
    Sh8601Driver<Ws18AmoledDriver, ResetDriver<SharedI2c>, embedded_graphics::pixelcolor::Rgb888>;
type BoardTouch = Ft3x68Driver<SharedI2c, Delay, TouchResetDriver<SharedI2c>>;

/// Board hardware constructed in [`init`] and consumed by the event loop.
struct BoardState {
    display: BoardDisplay,
    touch_driver: BoardTouch,
    pixel_buffer: Box<[Rgb8Pixel; FRAME_PIXELS]>,
}

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

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> {
        self.run_event_loop()
    }
}

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

/// Initializes the heap, the board peripherals and sets the Slint platform.
pub fn init() {
    let peripherals = esp_hal::init(esp_hal::Config::default().with_cpu_clock(CpuClock::_240MHz));
    init_logger_from_env();
    info!("Peripherals initialized");

    // Register the PSRAM heap before anything allocates. PSRAM is consumed here;
    // every other peripheral is used field by field below, so the whole
    // `Peripherals` struct is never stored and no `steal` is needed.
    esp_alloc::psram_allocator!(
        peripherals.PSRAM,
        esp_hal::psram,
        esp_hal::psram::PsramConfig {
            mode: esp_hal::psram::PsramMode::OctalSpi,
            ..Default::default()
        }
    );

    let delay = Delay::new();

    // --- Begin SPI and Display Initialization ---
    // DMA Buffers for SPI
    let (rx_buffer, rx_descriptors, tx_buffer, tx_descriptors) = dma_buffers!(DMA_CHUNK_SIZE);
    let dma_rx_buf = DmaRxBuf::new(rx_descriptors, rx_buffer).unwrap();
    let dma_tx_buf = DmaTxBuf::new(tx_descriptors, tx_buffer).unwrap();

    // SPI Configuration for Waveshare ESP32-S3 1.8inch AMOLED Touch Display
    let lcd_spi = Spi::new(
        peripherals.SPI2,
        SpiConfig::default().with_frequency(Rate::from_mhz(40_u32)).with_mode(Mode::_0),
    )
    .unwrap()
    .with_sio0(peripherals.GPIO4)
    .with_sio1(peripherals.GPIO5)
    .with_sio2(peripherals.GPIO6)
    .with_sio3(peripherals.GPIO7)
    .with_cs(peripherals.GPIO12)
    .with_sck(peripherals.GPIO11)
    .with_dma(peripherals.DMA_CH0)
    .with_buffers(dma_rx_buf, dma_tx_buf);

    // Shared I2C bus for Waveshare ESP32-S3 1.8inch AMOLED Touch Display
    // Using embedded-hal-bus RefCellDevice for shared access
    let i2c_instance =
        I2c::new(peripherals.I2C0, I2cConfig::default().with_frequency(Rate::from_khz(400)))
            .unwrap()
            .with_sda(peripherals.GPIO15)
            .with_scl(peripherals.GPIO14);

    // Use StaticCell to create a shared I2C bus
    static I2C_BUS: StaticCell<RefCell<I2c<'static, esp_hal::Blocking>>> = StaticCell::new();
    let i2c_bus = I2C_BUS.init(RefCell::new(i2c_instance));

    // Initialize the FT3x68 touch driver FIRST using shared I2C bus
    info!("Initializing FT3x68 Touch Driver first...");

    let touch_reset = TouchResetDriver::new(RefCellDevice::new(i2c_bus));
    let mut touch_driver =
        Ft3x68Driver::new(RefCellDevice::new(i2c_bus), FT3168_DEVICE_ADDRESS, touch_reset, delay);
    touch_driver.initialize().expect("Failed to initialize touch driver");
    info!("Touch driver initialized successfully");

    // NOW initialize I2C GPIO Reset Pin for the WaveShare 1.8" AMOLED display
    let reset = ResetDriver::new(RefCellDevice::new(i2c_bus));

    // Initialize display driver for the Waveshare 1.8" AMOLED display
    let ws_driver = Ws18AmoledDriver::new(lcd_spi);

    // Instantiate and Initialize Display AFTER touch
    info!("Initializing SH8601 Display after touch...");
    let display = Sh8601Driver::new_heap::<_, FB_SIZE>(
        ws_driver,
        reset,
        ColorMode::Rgb888,
        DISPLAY_SIZE,
        delay,
    )
    .expect("Display initialization failed");

    info!("Display initialized successfully after touch");

    // Create a pixel buffer for Slint to render into (allocate once).
    let pixel_buffer: Box<[Rgb8Pixel; FRAME_PIXELS]> =
        Box::new([Rgb8Pixel { r: 0, g: 0, b: 0 }; FRAME_PIXELS]);

    slint::platform::set_platform(Box::new(EspBackend {
        window: RefCell::new(None),
        state: RefCell::new(Some(BoardState { display, touch_driver, pixel_buffer })),
    }))
    .expect("backend already initialized");
}

impl EspBackend {
    fn run_event_loop(&self) -> Result<(), slint::PlatformError> {
        let BoardState { mut display, mut touch_driver, mut pixel_buffer } =
            self.state.borrow_mut().take().expect("event loop already running");
        let pixel_buf: &mut [Rgb8Pixel] = &mut *pixel_buffer;
        let mut delay = Delay::new();

        // Update the Slint window size from the display
        let size = slint::PhysicalSize::new(DISPLAY_SIZE.width as u32, DISPLAY_SIZE.height as u32);
        self.window.borrow().as_ref().unwrap().set_size(size);

        // Variable to track the last touch position
        let mut last_touch: Option<LogicalPosition> = None;

        info!("Entering main event loop...");

        // Main event loop
        loop {
            slint::platform::update_timers_and_animations();

            if let Some(window) = self.window.borrow().clone() {
                // Poll touch input
                match touch_driver.touch1() {
                    Ok(touch_state) => {
                        match touch_state {
                            ft3x68_rs::TouchState::Pressed(touch_point) => {
                                info!("Touch detected: x={}, y={}", touch_point.x, touch_point.y);

                                // Convert touch coordinates to logical position
                                let pos = PhysicalPosition::new(
                                    touch_point.x as i32,
                                    touch_point.y as i32,
                                )
                                .to_logical(window.scale_factor());

                                info!(
                                    "Converted to logical position: {:?}, scale_factor: {}",
                                    pos,
                                    window.scale_factor()
                                );

                                if let Some(prev_pos) = last_touch.replace(pos) {
                                    // If position changed, send a PointerMoved event
                                    if prev_pos != pos {
                                        info!("Sending PointerMoved event");
                                        let _ = window.dispatch_event_with_result(
                                            WindowEvent::PointerMoved { position: pos },
                                        );
                                    }
                                } else {
                                    // No previous touch, send a PointerPressed event
                                    info!("Sending PointerPressed event");
                                    let _ = window.dispatch_event_with_result(
                                        WindowEvent::PointerPressed {
                                            position: pos,
                                            button: PointerEventButton::Left,
                                        },
                                    );
                                }
                            }
                            ft3x68_rs::TouchState::Released => {
                                // Touch was released, send PointerReleased if we had a previous touch
                                if let Some(pos) = last_touch.take() {
                                    info!("Touch released, sending PointerReleased event");
                                    let _ = window.dispatch_event_with_result(
                                        WindowEvent::PointerReleased {
                                            position: pos,
                                            button: PointerEventButton::Left,
                                        },
                                    );
                                    let _ = window
                                        .dispatch_event_with_result(WindowEvent::PointerExited);
                                }
                            }
                        }
                    }
                    Err(e) => {
                        // I2C error or other driver error - send release if we had a previous touch
                        if let Some(pos) = last_touch.take() {
                            info!("Touch driver error, sending PointerReleased event: {:?}", e);
                            let _ =
                                window.dispatch_event_with_result(WindowEvent::PointerReleased {
                                    position: pos,
                                    button: PointerEventButton::Left,
                                });
                            let _ = window.dispatch_event_with_result(WindowEvent::PointerExited);
                        }
                        // Don't log every "no touch" - only log actual errors
                        // info!("Touch error: {:?}", e);
                    }
                }

                // Render the window if needed
                window.draw_if_needed(|renderer| {
                    renderer.render(pixel_buf, DISPLAY_SIZE.width as usize);
                });

                // Draw the rendered pixels to the display using draw_iter
                use embedded_graphics::Pixel;
                use embedded_graphics::prelude::*;

                let pixels = pixel_buf
                    .chunks_exact(DISPLAY_SIZE.width as usize)
                    .enumerate()
                    .flat_map(|(y, row)| {
                        row.iter().enumerate().map(move |(x, pixel)| {
                            let point = embedded_graphics::geometry::Point::new(x as i32, y as i32);
                            let color = embedded_graphics::pixelcolor::Rgb888::new(
                                pixel.r, pixel.g, pixel.b,
                            );
                            Pixel(point, color)
                        })
                    });

                let _ = display.draw_iter(pixels);

                // Flush the display to show the rendered content
                let _ = display.flush();

                if window.has_active_animations() {
                    continue;
                }
            }

            // Small delay to prevent busy waiting
            delay.delay_ms(16); // ~60 FPS
        }
    }
}
