use embassy_time::{Duration, Instant, Timer};
use esp_hal::spi::Mode;
use esp_hal::spi::master::{AnySpi, Config, Spi};
use esp_hal::{gpio::AnyPin, time::Rate, timer::timg::Wdt};
use esp_hal::dma::{AnyGdmaChannel, DmaTxBuf};
use figments::prelude::*;
use figments_esp32_ws2812_dma::{DmaBuffers, Esp32Ws2812SpiDmaWriter};
use figments_render::gamma::GammaCurve;
use figments_render::output::{GammaCorrected, OutputAsync};
use log::*;

use crate::graphics::display::NUM_PIXELS;
use crate::{graphics::display::{BikeOutput, DisplayControls, Uniforms}, tasks::ui::UiSurfacePool};

const POWER_MA : u32 = 300;
const POWER_VOLTS : u32 = 5;
const MAX_POWER_MW : u32 = POWER_VOLTS * POWER_MA;
static SPI_BUFFERS: static_cell::ConstStaticCell<DmaBuffers<u8, {NUM_PIXELS * 12 + 140}>> = static_cell::ConstStaticCell::new(DmaBuffers::new(0));

#[embassy_executor::task]
pub async fn render(spi: AnySpi<'static>, dma: AnyGdmaChannel<'static>, gpio: AnyPin<'static>, mut surfaces: UiSurfacePool, mut safety_surfaces: UiSurfacePool, mut controls: DisplayControls, mut wdt: Wdt<esp_hal::peripherals::TIMG0<'static>>) {
    info!("Starting rendering task");

    let buffers = SPI_BUFFERS.take();

    let tx_buf = DmaTxBuf::new(&mut buffers.tx_descriptors, &mut buffers.tx_buffer).unwrap();
    let conf = Config::default()
        .with_frequency(Rate::from_khz(3_800))
        .with_mode(Mode::_0);

    let driver = Spi::new(spi, conf).unwrap()
        .with_mosi(gpio)
        .with_dma(dma).into_async();

    let target = Esp32Ws2812SpiDmaWriter::new(driver, tx_buf);

    // This value is used as the 'seed' for rendering each frame, allowing us to do things like run the animation backwards, frames for double FPS, or even use system uptime for more human-paced animations
    let mut uniforms = Uniforms {
        primary_color: Hsv::new(210, 255, 255),
        ..Uniforms::default()
    };

    let mut output = BikeOutput::new(target, MAX_POWER_MW, controls.clone());
    output.set_gamma(GammaCurve::new(1.3));

    info!("Rendering started! {}ms since boot", Instant::now().as_millis());
    controls.notify_render_is_running(true);

    // TODO: The prediction engine should be able to scale the display FPS down for power saving sometimes
    const FPS: u64 = 30;
    const RENDER_BUDGET: Duration = Duration::from_millis(1000 / FPS);

    const ANIMATION_TPS: u64 = 120;
    const ANIMATION_FRAME_TIME: Duration = Duration::from_millis(1000 / ANIMATION_TPS);

    loop {
        // FIXME: need to put the rendering loop into a deep sleep when the display is off
        let start = Instant::now();

        output.blank();

        uniforms.frame = (Instant::now().as_millis() / ANIMATION_FRAME_TIME.as_millis()) as usize;
        surfaces.commit();
        safety_surfaces.commit();
        surfaces.render_to(&mut output, &uniforms);
        safety_surfaces.render_to(&mut output, &uniforms);

        // Finally, write out the rendered frame
        output.commit_async().await.expect("Failed to commit frame");

        let render_duration = Instant::now() - start;

        if !controls.is_on() {
            warn!("Renderer is sleeping zzzz");
            controls.notify_render_is_running(false);
            output.blank();
            output.commit_async().await.expect("Failed to commit low power frame");
            wdt.disable();
            controls.wait_until_display_is_on().await;
            wdt.feed();
            wdt.enable();
            warn!("Renderer is awake !!!!");
            controls.notify_render_is_running(true);
        }

        // Apply the FPS cap where we sleep if we are rendering fast enough
        if render_duration < RENDER_BUDGET {
            let remaining_budget = RENDER_BUDGET - render_duration;
            Timer::after(remaining_budget).await;
        } else {
            // Otherwise, we have a problem
            // TODO: Automatically scale FPS back when this happens
            warn!("Render stall! Frame took {}ms", render_duration.as_millis());
            // Kick the scheduler since it took so long
            Timer::after_ticks(1).await;
        }

        // TODO: Need a way to let the UI layers configure this
        uniforms.primary_color.hue = uniforms.primary_color.hue.wrapping_add(1);

        wdt.feed();
    }
}