use embassy_time::Timer;
use esp_hal::{
    DriverMode,
    gpio::{Flex, Level, Output},
    lcd_cam::lcd::{
        ClockMode, DelayMode, Phase, Polarity,
        dpi::{Dpi, Format, FrameTiming},
    },
    ledc::{self, LowSpeed, channel::ChannelIFace, timer::TimerIFace},
    time::Rate,
};
use log::debug;
use ouroboros::self_referencing;

use crate::peripherals::st7701s::{commands::Command, init_sequence::InitSequenceAction};

mod commands;
mod init_sequence;
mod spi;

#[self_referencing]
pub struct Backlight<'d> {
    pub timer: ledc::timer::Timer<'d, LowSpeed>,
    #[borrows(timer)]
    #[covariant]
    pub channel: ledc::channel::Channel<'this, LowSpeed>,
}

impl<'a> Backlight<'a> {
    pub fn set_duty(&mut self, duty_pct: u8) -> Result<(), ledc::channel::Error> {
        self.borrow_channel().set_duty(duty_pct)
    }
}

pub struct St7701sController<'d> {
    pub sck: Output<'d>,
    pub mosi: Flex<'d>,
    pub cs: Output<'d>,
    pub backlight: Backlight<'d>,
}

impl<'d> St7701sController<'d> {
    pub async fn send_init_sequence(&mut self) {
        debug!("Writing ST7701S init sequence.");

        for action in &init_sequence::INIT_SEQUENCE_COMMANDS {
            match action {
                InitSequenceAction::Command(command) => {
                    spi::spi_write(
                        command.address(),
                        command.args_iter().copied(),
                        &mut self.mosi,
                        &mut self.sck,
                        &mut self.cs,
                    )
                    .await;
                }
                InitSequenceAction::SleepMs(delay_ms) => {
                    Timer::after_millis(*delay_ms).await;
                }
            }
        }
    }

    pub async fn send(&mut self, command: impl Command) {
        spi::spi_write(
            command.address(),
            command.args_iter().copied(),
            &mut self.mosi,
            &mut self.sck,
            &mut self.cs,
        )
        .await;
    }

    /// Puts the display into sleep mode and disables the backlight.
    pub async fn sleep_on(&mut self) {
        self.backlight.set_duty(0).unwrap();
        self.send(commands::CmdSlpin()).await;
    }

    /// Brings the display out of sleep mode and enables the backlight.
    pub async fn sleep_off(&mut self) {
        self.send(commands::CmdSlpout()).await;
        self.backlight.set_duty(100).unwrap();
    }
}

pub struct St7701s<'d, Dm>
where
    Dm: DriverMode,
{
    pub controller: St7701sController<'d>,
    pub dpi: Dpi<'d, Dm>,
}

impl<'d, Dm> St7701s<'d, Dm>
where
    Dm: DriverMode,
{
    /// Initializes an ST7701S display, and puts it to sleep.
    /// To turn it on, invoke `St7701sController:sleep_off`.
    pub async fn new(
        mut sck: Output<'d>,
        mut mosi: Flex<'d>,
        mut cs: Output<'d>,
        mut unconfigured_dpi: Dpi<'d, Dm>,
        mut bl_timer: ledc::timer::Timer<'d, LowSpeed>,
        bl_channel: ledc::channel::Channel<'d, LowSpeed>,
    ) -> Self {
        sck.apply_config(&Default::default());
        sck.set_high();
        cs.apply_config(&Default::default());
        cs.set_high();
        mosi.apply_input_config(&Default::default());
        mosi.apply_output_config(&Default::default());
        mosi.set_input_enable(false);
        mosi.set_output_enable(true);

        let lcd_config = esp_hal::lcd_cam::lcd::dpi::Config::default()
            // Internal memory can use the full 16 MHz, but when external PSRAM is used, it cannot keep up with the display.
            // For that reason, we choose the highest value for which it doesn't glitch by showing black
            // stripes on the screen.
            //
            // There are three knobs you can turn to improve the bandwidth situation.
            // - increase psram frequency
            // - decrease the peripheral's frequency
            // - prevent flash from being used whilst your program is running. (There's a PR to make
            //   this easy to do)
            //   https://github.com/esp-rs/esp-hal/pull/3024
            //
            // Adafruit would use 11 MHz.
            // I had lowered the frequency, so that `DmaBounce` could keep up.
            .with_frequency(Rate::from_mhz(9)) // From Adafruit
            .with_clock_mode(ClockMode {
                polarity: Polarity::IdleLow, // From Adafruit
                phase: Phase::ShiftHigh,     // From Adafruit
            })
            .with_de_mode(DelayMode::RaisingEdge)
            .with_hsync_mode(DelayMode::RaisingEdge)
            .with_vsync_mode(DelayMode::RaisingEdge)
            .with_output_bit_mode(DelayMode::RaisingEdge)
            .with_format(Format {
                enable_2byte_mode: true,
                ..Default::default()
            })
            .with_timing({
                // Adafruit's config for this LCD:
                // https://github.com/adafruit/Adafruit_CircuitPython_Qualia/blob/742d336e05e6a4d8bdaa46e15bbf60c9f30d2eba/adafruit_qualia/displays/bar240x960.py#L81-L97
                // https://github.com/adafruit/Adafruit_CircuitPython_Qualia/blob/742d336e05e6a4d8bdaa46e15bbf60c9f30d2eba/adafruit_qualia/displays/__init__.py#L59-L62
                // CircuitPython code handling Adafruit's config
                // https://github.com/adafruit/circuitpython/blob/97c6617817e95b1f6aa2ce458778aaa8371de39b/ports/espressif/common-hal/dotclockframebuffer/DotClockFramebuffer.c#L63
                // ESP-IDF peripheral configuration code:
                // https://github.com/espressif/esp-idf/blob/800f141f94c0f880c162de476512e183df671307/components/esp_lcd/rgb/esp_lcd_panel_rgb.c#L556
                // Espressif's docs:
                // https://docs.espressif.com/projects/esp-idf/en/v5.5.1/esp32s3/api-reference/peripherals/lcd/rgb_lcd.html#structures

                // TODO: Investigate PORCTRL instruction in datasheet of ST7701
                let horizontal_resolution: usize = 240;
                let vertical_resolution = 960;
                let overscan_left = 120;
                let vsync_width = 8;
                let hsync_width = 8;
                let horizontal_blank_front_porch = 20;
                let horizontal_blank_back_porch = 20;
                let vertical_blank_front_porch = 20;
                let vertical_blank_back_porch = 20;
                let hsync_position = 0;
                let horizontal_active_width =
                    (horizontal_resolution + overscan_left).div_ceil(16) * 16; // Round up to a multiple of 16.
                let vertical_active_height = vertical_resolution;
                FrameTiming {
                    horizontal_total_width: hsync_width
                        + horizontal_blank_back_porch
                        + horizontal_active_width
                        + horizontal_blank_front_porch,
                    vertical_total_height: vsync_width
                        + vertical_blank_back_porch
                        + vertical_active_height
                        + vertical_blank_front_porch,
                    horizontal_blank_front_porch: horizontal_blank_front_porch + hsync_width,
                    vertical_blank_front_porch: vertical_blank_front_porch + vsync_width,
                    horizontal_active_width,
                    vertical_active_height,
                    vsync_width,
                    hsync_width,
                    hsync_position,
                }
            })
            .with_hsync_idle_level(Level::High)
            .with_vsync_idle_level(Level::High)
            .with_de_idle_level(Level::Low);

        unconfigured_dpi.apply_config(&lcd_config).unwrap();

        bl_timer
            .configure(ledc::timer::config::Config {
                duty: ledc::timer::config::Duty::Duty5Bit,
                clock_source: ledc::timer::LSClockSource::APBClk,
                frequency: Rate::from_khz(24),
            })
            .unwrap();
        let backlight = Backlight::new(bl_timer, move |bl_timer| {
            let mut bl_channel = bl_channel; // Forces bl_channel to be moved before it is mutated.
            bl_channel
                .configure(ledc::channel::config::Config {
                    timer: bl_timer,
                    drive_mode: esp_hal::gpio::DriveMode::PushPull,
                    duty_pct: 0,
                })
                .unwrap();
            bl_channel
        });

        let mut lcd = Self {
            controller: St7701sController {
                sck,
                mosi,
                cs,
                backlight,
            },
            dpi: unconfigured_dpi,
        };

        lcd.controller.send_init_sequence().await;
        lcd.controller.sleep_on().await;

        lcd
    }
}
