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

// cSpell: ignore datasheet
#[panic_handler]
fn panic(info: &core::panic::PanicInfo) -> ! {
    esp_println::println!("Panic: {:?}", info);
    loop {}
}
esp_bootloader_esp_idf::esp_app_desc!();

use alloc::boxed::Box;
use alloc::rc::Rc;
use core::cell::RefCell;
use embedded_graphics_core::draw_target::DrawTarget;
use embedded_graphics_core::pixelcolor::RgbColor;
use embedded_hal::delay::DelayNs;
use embedded_hal::digital::OutputPin;
use embedded_hal_bus::spi::ExclusiveDevice;
use esp_alloc as _;
use esp_backtrace as _;
use esp_hal::clock::CpuClock;
use esp_hal::time::Instant;
use esp_hal::{
    delay::Delay,
    gpio::{Level, Output, OutputConfig},
    i2c::master::I2c,
    spi::Mode as SpiMode,
    spi::master::{Config as SpiConfig, Spi},
    time::Rate,
};
use esp_println::logger::init_logger_from_env;
use log::{error, info};
use mipidsi::options::{ColorInversion, ColorOrder};

// Touch support imports
use embedded_hal_bus::i2c::RefCellDevice;
use ft3x68_rs::{Ft3x68Driver, ResetInterface};
use slint::PhysicalPosition;
use slint::platform::{PointerEventButton, WindowEvent};
use static_cell::StaticCell;

// FT6336U I2C address (compatible with FT3x68 driver)
const FT6336U_DEVICE_ADDRESS: u8 = 0x38;

// AW9523 I2C address
const AW9523_I2C_ADDRESS: u8 = 0x58;

/// Touch reset implementation via AW9523 GPIO expander using direct I2C commands
/// Based on the AW9523 datasheet and M5Stack CoreS3 schematics
pub struct TouchResetDriverAW9523<I2C> {
    i2c: I2C,
}

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

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

    fn reset(&mut self) -> Result<(), Self::Error> {
        let delay = Delay::new();

        // AW9523 register addresses:
        // 0x02: Port 0 Configuration (0=output, 1=input)
        // 0x03: Port 1 Configuration (0=output, 1=input)
        // 0x04: Port 0 Output (pin values for outputs)
        // 0x05: Port 1 Output (pin values for outputs)

        // Configure P0_0 (touch reset) as output (bit 0 = 0)
        // Keep other pins as they are - read current config first
        let mut config_p0 = [0u8; 1];
        self.i2c.write_read(AW9523_I2C_ADDRESS, &[0x02], &mut config_p0)?;
        let new_config_p0 = config_p0[0] & !0x01; // Clear bit 0 to make P0_0 output
        self.i2c.write(AW9523_I2C_ADDRESS, &[0x02, new_config_p0])?;

        // Pull reset (P0_0) low
        let mut output_p0 = [0u8; 1];
        self.i2c.write_read(AW9523_I2C_ADDRESS, &[0x04], &mut output_p0)?;
        let new_output_low = output_p0[0] & !0x01; // Clear bit 0 to pull P0_0 low
        self.i2c.write(AW9523_I2C_ADDRESS, &[0x04, new_output_low])?;
        delay.delay_millis(10);

        // Pull reset (P0_0) high
        let new_output_high = output_p0[0] | 0x01; // Set bit 0 to pull P0_0 high
        self.i2c.write(AW9523_I2C_ADDRESS, &[0x04, new_output_high])?;
        delay.delay_millis(300);

        Ok(())
    }
}

type SharedI2c = RefCellDevice<'static, I2c<'static, esp_hal::Blocking>>;
type BoardDisplay = mipidsi::Display<
    mipidsi::interface::SpiInterface<
        'static,
        ExclusiveDevice<Spi<'static, esp_hal::Blocking>, Output<'static>, Delay>,
        Output<'static>,
    >,
    mipidsi::models::ILI9342CRgb565,
    Output<'static>,
>;
type BoardTouch = Ft3x68Driver<SharedI2c, Delay, TouchResetDriverAW9523<SharedI2c>>;

/// Board hardware constructed in [`init`] and consumed by the event loop.
struct BoardState {
    buffer_provider: DrawBuffer<'static, BoardDisplay>,
    touch_driver: BoardTouch,
    // Kept alive for the lifetime of the event loop.
    _backlight: Output<'static>,
}

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::QuadSpi,
            ..Default::default()
        }
    );

    let mut delay = Delay::new();

    // --- Initialize I2C bus for all I2C devices (AXP2101, AW9523, touch controller) ---
    let power_i2c = I2c::new(
        peripherals.I2C0,
        esp_hal::i2c::master::Config::default().with_frequency(Rate::from_khz(400)),
    )
    .unwrap()
    .with_sda(peripherals.GPIO12) // AXP2101 SDA
    .with_scl(peripherals.GPIO11); // AXP2101 SCL

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

    // --- Begin AXP2101 Power Management Initialization ---
    // Initialize power management using shared I2C bus - critical for M5Stack CoreS3
    init_axp2101_power(RefCellDevice::new(i2c_bus))
        .expect("Failed to initialize AXP2101 power management");
    info!("Power management initialized successfully");

    // Small delay to let power rails stabilize
    delay.delay_ms(100);

    // --- Begin AW9523 GPIO Expander Initialization ---
    // Initialize AW9523 GPIO expander using M5Stack CoreS3 specific sequence
    init_aw9523_gpio_expander(RefCellDevice::new(i2c_bus))
        .expect("Failed to initialize AW9523 GPIO expander");
    info!("AW9523 GPIO expander initialized successfully");
    // --- End AW9523 Initialization ---

    // --- Begin SPI and Display Initialization ---
    let spi = Spi::<esp_hal::Blocking>::new(
        peripherals.SPI2,
        SpiConfig::default().with_frequency(Rate::from_mhz(40)).with_mode(SpiMode::_0),
    )
    .unwrap()
    .with_sck(peripherals.GPIO36) // SPI Clock
    .with_mosi(peripherals.GPIO37); // SPI MOSI

    // Display control pins
    let dc = Output::new(peripherals.GPIO35, Level::Low, OutputConfig::default()); // D/C pin
    let cs = Output::new(peripherals.GPIO3, Level::High, OutputConfig::default()); // CS pin
    let reset = Output::new(peripherals.GPIO15, Level::High, OutputConfig::default()); // Reset pin

    // Wrap SPI into a bus. The interface buffer must outlive the display, so it
    // lives in a `StaticCell`.
    let spi_device = ExclusiveDevice::new(spi, cs, Delay::new()).unwrap();
    static SPI_BUFFER: StaticCell<[u8; 512]> = StaticCell::new();
    let di = mipidsi::interface::SpiInterface::new(spi_device, dc, SPI_BUFFER.init([0u8; 512]));

    // Add small delay before display initialization
    delay.delay_ms(10);

    // Initialize the display with settings
    let mut display = mipidsi::Builder::new(mipidsi::models::ILI9342CRgb565, di)
        .reset_pin(reset)
        .display_size(320, 240)
        .color_order(ColorOrder::Bgr)
        .invert_colors(ColorInversion::Inverted)
        .init(&mut delay)
        .unwrap();

    // Clear display to test it's working
    use embedded_graphics::pixelcolor::Rgb565;
    display.clear(Rgb565::BLUE).expect("Display clear failed");
    info!("Display initialized and cleared to blue");

    // Set up the backlight pin (controlled via AXP2101, but we can use GPIO for basic control)
    let mut backlight = Output::new(peripherals.GPIO16, Level::Low, OutputConfig::default());
    backlight.set_high(); // Enable backlight
    // --- End Display Initialization ---

    // --- Begin Touch Initialization ---
    info!("Initializing FT6336U touch controller...");

    // Create touch reset driver using shared I2C bus
    let touch_reset = TouchResetDriverAW9523::new(RefCellDevice::new(i2c_bus));

    // Initialize FT6336U touch driver using shared I2C bus
    let mut touch_driver =
        Ft3x68Driver::new(RefCellDevice::new(i2c_bus), FT6336U_DEVICE_ADDRESS, touch_reset, delay);

    match touch_driver.initialize() {
        Ok(_) => info!("FT6336U touch controller initialized successfully"),
        Err(e) => {
            error!("Touch initialization failed: {:?}", e);
            // Continue without touch
        }
    }
    // --- End Touch Initialization ---

    // Prepare a draw buffer for the Slint software renderer. The line buffer must
    // outlive the platform, so it lives in a `StaticCell`.
    static LINE_BUFFER: StaticCell<[slint::platform::software_renderer::Rgb565Pixel; 320]> =
        StaticCell::new();
    let buffer_provider = DrawBuffer {
        display,
        buffer: LINE_BUFFER.init([slint::platform::software_renderer::Rgb565Pixel(0); 320]),
    };

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

/// Initialize the AXP2101 power management unit for M5Stack CoreS3 using shared I2C
/// This implements the exact same sequence as the working custom implementation
/// Based on https://github.com/tuupola/axp192
/// and https://github.com/m5stack/M5CoreS3/blob/main/src/AXP2101.cpp
fn init_axp2101_power<I2C>(mut i2c_device: I2C) -> Result<(), ()>
where
    I2C: embedded_hal::i2c::I2c,
{
    const AXP2101_ADDRESS: u8 = 0x34;

    info!("Initializing AXP2101 power management with M5Stack CoreS3 sequence...");

    // This sequence matches exactly the working custom implementation:
    // 1. CHG_LED register (0x69) <- 0x35 (0b00110101)
    // 2. ALDO_ENABLE register (0x90) <- 0xBF
    // 3. ALDO4 register (0x95) <- 0x1C (0b00011100)

    // Step 1: Configure charge LED (register 0x69 = 105 decimal)
    if i2c_device.write(AXP2101_ADDRESS, &[0x69, 0x35]).is_err() {
        error!("Failed to write to CHG_LED register (0x69)");
        return Err(());
    }
    info!("AXP2101: CHG_LED configured (0x69 <- 0x35)");

    // Step 2: Enable ALDO outputs (register 0x90 = 144 decimal)
    if i2c_device.write(AXP2101_ADDRESS, &[0x90, 0xBF]).is_err() {
        error!("Failed to write to ALDO_ENABLE register (0x90)");
        return Err(());
    }
    info!("AXP2101: ALDO outputs enabled (0x90 <- 0xBF)");

    // Step 3: Configure ALDO4 voltage (register 0x95 = 149 decimal)
    if i2c_device.write(AXP2101_ADDRESS, &[0x95, 0x1C]).is_err() {
        error!("Failed to write to ALDO4 register (0x95)");
        return Err(());
    }
    info!("AXP2101: ALDO4 voltage configured (0x95 <- 0x1C)");

    info!("AXP2101 power management initialized successfully with M5Stack CoreS3 sequence");
    Ok(())
}

/// Initialize the AW9523 GPIO expander for M5Stack CoreS3 using shared I2C
/// This implements the exact same sequence as the working custom implementation
/// Based on: https://github.com/m5stack/M5CoreS3/blob/main/src/AXP2101.cpp
fn init_aw9523_gpio_expander<I2C>(mut i2c_device: I2C) -> Result<(), ()>
where
    I2C: embedded_hal::i2c::I2c,
{
    info!("Initializing AW9523 GPIO expander with M5Stack CoreS3 sequence...");

    // Step 1: Configure Port 0 Configuration (register 0x02) <- 0b00000101 (0x05)
    if i2c_device.write(AW9523_I2C_ADDRESS, &[0x02, 0b00000101]).is_err() {
        error!("Failed to write to AW9523 Port 0 Configuration register (0x02)");
        return Err(());
    }
    info!("AW9523: Port 0 Configuration set (0x02 <- 0x05)");

    // Step 2: Configure Port 1 Configuration (register 0x03) <- 0b00000011 (0x03)
    if i2c_device.write(AW9523_I2C_ADDRESS, &[0x03, 0b00000011]).is_err() {
        error!("Failed to write to AW9523 Port 1 Configuration register (0x03)");
        return Err(());
    }
    info!("AW9523: Port 1 Configuration set (0x03 <- 0x03)");

    // Step 3: Configure Port 0 Output (register 0x04) <- 0b00011000 (0x18)
    if i2c_device.write(AW9523_I2C_ADDRESS, &[0x04, 0b00011000]).is_err() {
        error!("Failed to write to AW9523 Port 0 Output register (0x04)");
        return Err(());
    }
    info!("AW9523: Port 0 Output set (0x04 <- 0x18)");

    // Step 4: Configure Port 1 Output (register 0x05) <- 0b00001100 (0x0C)
    if i2c_device.write(AW9523_I2C_ADDRESS, &[0x05, 0b00001100]).is_err() {
        error!("Failed to write to AW9523 Port 1 Output register (0x05)");
        return Err(());
    }
    info!("AW9523: Port 1 Output set (0x05 <- 0x0C)");

    // Step 5: Configure register 0x11 <- 0b00010000 (0x10)
    if i2c_device.write(AW9523_I2C_ADDRESS, &[0x11, 0b00010000]).is_err() {
        error!("Failed to write to AW9523 register (0x11)");
        return Err(());
    }
    info!("AW9523: Register 0x11 configured (0x11 <- 0x10)");

    // Step 6: Configure register 0x13 <- 0b11111111 (0xFF)
    if i2c_device.write(AW9523_I2C_ADDRESS, &[0x13, 0b11111111]).is_err() {
        error!("Failed to write to AW9523 register (0x13)");
        return Err(());
    }
    info!("AW9523: Register 0x13 configured (0x13 <- 0xFF)");

    info!("AW9523 GPIO expander initialized successfully with M5Stack CoreS3 sequence");
    Ok(())
}

impl EspBackend {
    fn run_event_loop(&self) -> Result<(), slint::PlatformError> {
        let BoardState { mut buffer_provider, mut touch_driver, _backlight } =
            self.state.borrow_mut().take().expect("event loop already running");

        // Update the Slint window size from the display (320x240 for M5Stack CoreS3)
        let size = slint::PhysicalSize::new(320, 240);
        self.window.borrow().as_ref().unwrap().set_size(size);

        // Variable to track the last touch position
        let mut last_touch = None;

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

            if let Some(window) = self.window.borrow().clone() {
                // Poll touch input using FT3x68Driver
                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());

                                if let Some(prev_pos) = last_touch.replace(pos) {
                                    // If position changed, send a PointerMoved event
                                    if prev_pos != pos {
                                        let _ = window.dispatch_event_with_result(
                                            WindowEvent::PointerMoved { position: pos },
                                        );
                                    }
                                } else {
                                    // No previous touch, send a 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() {
                                    let _ = window.dispatch_event_with_result(
                                        WindowEvent::PointerReleased {
                                            position: pos,
                                            button: PointerEventButton::Left,
                                        },
                                    );
                                    let _ = window
                                        .dispatch_event_with_result(WindowEvent::PointerExited);
                                }
                            }
                        }
                    }
                    Err(_) => {
                        // Touch error - ignore and continue
                    }
                }

                // Render the window if needed
                window.draw_if_needed(|renderer| {
                    renderer.render_by_line(&mut buffer_provider);
                });

                if window.has_active_animations() {
                    continue;
                }
            }
        }
    }
}

/// Provides a draw buffer for the MinimalSoftwareWindow renderer.
struct DrawBuffer<'a, Display> {
    display: Display,
    buffer: &'a mut [slint::platform::software_renderer::Rgb565Pixel],
}

impl<
    DI: mipidsi::interface::Interface<Word = u8>,
    RST: OutputPin<Error = core::convert::Infallible>,
> slint::platform::software_renderer::LineBufferProvider
    for &mut DrawBuffer<'_, mipidsi::Display<DI, mipidsi::models::ILI9342CRgb565, RST>>
{
    type TargetPixel = slint::platform::software_renderer::Rgb565Pixel;

    fn process_line(
        &mut self,
        line: usize,
        range: core::ops::Range<usize>,
        render_fn: impl FnOnce(&mut [slint::platform::software_renderer::Rgb565Pixel]),
    ) {
        let buffer = &mut self.buffer[range.clone()];
        render_fn(buffer);

        // Update the display with the rendered line
        self.display
            .set_pixels(
                range.start as u16,
                line as u16,
                range.end as u16,
                line as u16,
                buffer
                    .iter()
                    .map(|x| embedded_graphics_core::pixelcolor::raw::RawU16::new(x.0).into()),
            )
            .unwrap();
    }
}
