//! Wi-Fi enabled 8x12 LED clock with captive-portal setup.
//!
//! Uses `WifiAuto` for setup and connection, including a timezone field.
//! The display shows hours on the top line and minutes on the bottom line.
//!
//! Hardware:
//! - 8x12 panel on GPIO18
//! - optional force-portal button on GPIO6 wired to GND

#![no_std]
#![no_main]

use device_envoy_example_common::clock_ui::{run_clock_ui, ClockUiEvent};
use embassy_executor::Spawner;
use esp_backtrace as _;
use log::info;

use device_envoy_esp::{
    button::PressedTo,
    button_watch,
    clock_sync::{ClockSyncEsp, ClockSyncStaticEsp, ONE_MINUTE},
    flash_block::FlashBlockEsp,
    init_and_start, led2d,
    led2d::Led2d as _,
    led2d::{layout::LedLayout, Led2dFont},
    led_strip::{colors, Current, Gamma},
    wifi_auto::{
        fields::{TimezoneField, TimezoneFieldStatic},
        WifiAuto as _, WifiAutoEsp, WifiAutoEvent,
    },
    Result,
};

esp_bootloader_esp_idf::esp_app_desc!();

const CAPTIVE_PORTAL_SSID: &str = "DeviceEnvoyClock";
const DIGIT_COLORS: [smart_leds::RGB8; 4] =
    [colors::CYAN, colors::MAGENTA, colors::ORANGE, colors::LIME];
const EDIT_COLORS: [smart_leds::RGB8; 4] = [
    colors::FIREBRICK,
    colors::DARK_ORANGE,
    colors::TEAL,
    colors::MAROON,
];

const LED_LAYOUT_12X4: LedLayout<48, 12, 4> = LedLayout::serpentine_column_major();
const LED_LAYOUT_8X12: LedLayout<96, 8, 12> =
    LED_LAYOUT_12X4.combine_v(LED_LAYOUT_12X4).rotate_cw();

button_watch! {
    ForcePortalButtonWatch {
        pin: GPIO6,
    }
}

led2d! {
    Led8x12Clock {
        pin: GPIO18,
        len: 96,
        led_layout: LED_LAYOUT_8X12,
        max_current: Current::Milliamps(250),
        font: Led2dFont::Font4x6Trim,
        gamma: Gamma::Linear,
        max_frames: 16,
    }
}

#[esp_rtos::main]
async fn main(spawner: Spawner) -> ! {
    match inner_main(spawner).await {
        Ok(infallible) => match infallible {},
        Err(error) => panic!("{error:?}"),
    }
}

async fn inner_main(spawner: Spawner) -> Result<core::convert::Infallible> {
    init_and_start!(p, rmt80: rmt80, mode: rmt_mode::Blocking);
    esp_println::logger::init_logger(log::LevelFilter::Info);

    info!("clock_led8x12 starting");

    let led8x12_clock = Led8x12Clock::new(p.GPIO18, rmt80.channel0, spawner)?;

    let [wifi_auto_flash_block, mut timezone_flash_block] = FlashBlockEsp::new_array::<2>(p.FLASH)?;
    static TIMEZONE_FIELD_STATIC: TimezoneFieldStatic = TimezoneField::new_static();
    let timezone_field = TimezoneField::new(&TIMEZONE_FIELD_STATIC, timezone_flash_block);
    let button6 = ForcePortalButtonWatch::new(p.GPIO6, PressedTo::Ground, spawner).await?;

    let wifi_auto = WifiAutoEsp::new(
        p.WIFI,
        wifi_auto_flash_block,
        CAPTIVE_PORTAL_SSID,
        [timezone_field],
        spawner,
    )?;

    let led8x12_clock_ref = &led8x12_clock;
    let stack = wifi_auto
        .connect(&mut *button6, |wifi_auto_event| async move {
            match wifi_auto_event {
                WifiAutoEvent::CaptivePortalReady => {
                    led8x12_clock_ref.write_text("JO\nIN", &DIGIT_COLORS);
                }
                WifiAutoEvent::Connecting { .. } => {
                    led8x12_clock_ref.write_text("CO\nNN", &DIGIT_COLORS);
                }
                WifiAutoEvent::ConnectionFailed => {
                    led8x12_clock_ref.write_text("FA\nIL", &DIGIT_COLORS);
                }
            }
            Ok(())
        })
        .await?;

    led8x12_clock.write_text("IP\n..", &DIGIT_COLORS);
    stack.wait_config_up().await;

    info!("network up: {:?}", stack.config_v4());

    let offset_minutes = timezone_field.offset_minutes()?.unwrap_or(0);
    info!("timezone offset minutes: {}", offset_minutes);

    static CLOCK_SYNC_STATIC: ClockSyncStaticEsp = ClockSyncEsp::new_static();
    let clock_sync = ClockSyncEsp::new(
        &CLOCK_SYNC_STATIC,
        stack,
        offset_minutes,
        Some(ONE_MINUTE),
        spawner,
    );

    let led8x12_clock_ref = led8x12_clock;
    run_clock_ui(
        &clock_sync,
        &mut *button6,
        &mut timezone_flash_block,
        |clock_ui_event| async move {
            match clock_ui_event {
                ClockUiEvent::RenderHoursMinutes { hours, minutes } => {
                    render_hours_minutes(led8x12_clock_ref, hours, minutes, &DIGIT_COLORS);
                }
                ClockUiEvent::RenderMinutesSeconds { minutes, seconds } => {
                    render_minutes_seconds(led8x12_clock_ref, minutes, seconds, &DIGIT_COLORS);
                }
                ClockUiEvent::RenderHoursMinutesEdit { hours, minutes } => {
                    render_hours_minutes(led8x12_clock_ref, hours, minutes, &EDIT_COLORS);
                }
            }
            Ok(())
        },
    )
    .await
}

fn render_hours_minutes(
    led8x12_clock: &'static Led8x12Clock,
    hours: u8,
    minutes: u8,
    colors: &[smart_leds::RGB8],
) {
    let display_text = format_hhmm(hours, minutes);
    let display_text_with_o = format_zero_as_o(&display_text);
    led8x12_clock.write_text(display_text_with_o.as_str(), colors);
}

fn render_minutes_seconds(
    led8x12_clock: &'static Led8x12Clock,
    minutes: u8,
    seconds: u8,
    colors: &[smart_leds::RGB8],
) {
    let display_text = format_mmss(minutes, seconds);
    let display_text_with_o = format_zero_as_o(&display_text);
    led8x12_clock.write_text(display_text_with_o.as_str(), colors);
}

fn format_hhmm(hour12: u8, minute: u8) -> heapless::String<5> {
    let mut text = heapless::String::<5>::new();
    if hour12 >= 10 {
        text.push(char::from(b'0' + (hour12 / 10))).ok();
    } else {
        text.push(' ').ok();
    }
    text.push(char::from(b'0' + (hour12 % 10))).ok();
    text.push('\n').ok();
    text.push(char::from(b'0' + (minute / 10))).ok();
    text.push(char::from(b'0' + (minute % 10))).ok();
    text
}

fn format_mmss(minute: u8, second: u8) -> heapless::String<5> {
    let mut text = heapless::String::<5>::new();
    text.push(char::from(b'0' + (minute / 10))).ok();
    text.push(char::from(b'0' + (minute % 10))).ok();
    text.push('\n').ok();
    text.push(char::from(b'0' + (second / 10))).ok();
    text.push(char::from(b'0' + (second % 10))).ok();
    text
}

fn format_zero_as_o(text: &str) -> heapless::String<5> {
    let mut formatted_text = heapless::String::<5>::new();
    for display_character in text.chars() {
        if display_character == '0' {
            formatted_text.push('O').ok();
        } else {
            formatted_text.push(display_character).ok();
        }
    }
    formatted_text
}
