//! Wi-Fi enabled 4-digit clock that provisions credentials through `WifiAuto`.
//!
//! This example demonstrates how to pair the shared captive-portal workflow with the
//! 4-digit LED clock state machine. The `WifiAuto` helper owns Wi-Fi onboarding while the
//! clock display reflects progress and, once connected, continues handling user input.
//!
//! Hardware defaults:
//! - force-portal button on GPIO6 (wired to GND)
//! - LED4 cell pins: GPIO14, GPIO13, GPIO12, GPIO11 (active-low)
//! - LED4 segment pins: GPIO10, GPIO9, GPIO46 (S3) / GPIO4 (C6), GPIO3, GPIO8, GPIO18, GPIO17, GPIO16 (active-high)

#![no_std]
#![no_main]
#![allow(clippy::future_not_send, reason = "single-threaded")]

use core::convert::Infallible;

use device_envoy_example_common::clock_ui::{run_clock_ui, ClockUiEvent};
use embassy_executor::Spawner;
use esp_backtrace as _;
use esp_hal::gpio::{Level, Output, OutputConfig};
use log::info;

use device_envoy_esp::{
    button::PressedTo,
    button_watch,
    clock_sync::{ClockSync as _, ClockSyncEsp, ClockSyncStaticEsp, ONE_MINUTE},
    flash_block::FlashBlockEsp,
    init_and_start,
    led4::{
        circular_outline_animation, BlinkState, Led4 as _, Led4Esp, Led4EspStatic, OutputArray,
    },
    wifi_auto::{
        fields::{TimezoneField, TimezoneFieldStatic},
        WifiAuto as _, WifiAutoEsp, WifiAutoEvent,
    },
    Error, Result,
};

esp_bootloader_esp_idf::esp_app_desc!();

const CAPTIVE_PORTAL_SSID: &str = "DeviceEnvoyClock";
button_watch! {
    ForcePortalButtonWatch {
        pin: GPIO6,
    }
}

#[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<Infallible> {
    init_and_start!(p);
    esp_println::logger::init_logger(log::LevelFilter::Info);

    info!("Starting Wi-Fi 4-digit clock (WifiAuto)");

    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 button_watch6 = 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 cell_pins = OutputArray::new([
        Output::new(p.GPIO14, Level::High, OutputConfig::default()),
        Output::new(p.GPIO13, Level::High, OutputConfig::default()),
        Output::new(p.GPIO12, Level::High, OutputConfig::default()),
        Output::new(p.GPIO11, Level::High, OutputConfig::default()),
    ]);

    let segment_pins = OutputArray::new([
        Output::new(p.GPIO10, Level::Low, OutputConfig::default()),
        Output::new(p.GPIO9, Level::Low, OutputConfig::default()),
        #[cfg(target_arch = "xtensa")]
        Output::new(p.GPIO46, Level::Low, OutputConfig::default()),
        #[cfg(not(target_arch = "xtensa"))]
        Output::new(p.GPIO4, Level::Low, OutputConfig::default()),
        Output::new(p.GPIO3, Level::Low, OutputConfig::default()),
        Output::new(p.GPIO8, Level::Low, OutputConfig::default()),
        Output::new(p.GPIO18, Level::Low, OutputConfig::default()),
        Output::new(p.GPIO17, Level::Low, OutputConfig::default()),
        Output::new(p.GPIO16, Level::Low, OutputConfig::default()),
    ]);

    static LED4_STATIC: Led4EspStatic = Led4Esp::new_static();
    let led4 = Led4Esp::new(&LED4_STATIC, cell_pins, segment_pins, spawner)?;

    let led4_ref = &led4;
    let stack = wifi_auto
        .connect(&mut *button_watch6, |wifi_auto_event| async move {
            match wifi_auto_event {
                WifiAutoEvent::CaptivePortalReady => {
                    led4_ref.write_text(['j', 'o', 'i', 'n'], BlinkState::BlinkingAndOn);
                }
                WifiAutoEvent::Connecting { .. } => {
                    led4_ref.animate_text(circular_outline_animation(true));
                }
                WifiAutoEvent::ConnectionFailed => {
                    led4_ref.write_text(['F', 'A', 'I', 'L'], BlinkState::BlinkingButOff);
                }
            }
            Ok(())
        })
        .await?;

    led4.write_text(['D', 'O', 'N', 'E'], BlinkState::Solid);
    info!("WiFi connected");

    let offset_minutes = timezone_field
        .offset_minutes()?
        .ok_or(Error::MissingCustomWifiAutoField)?;

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

    led4.animate_text(circular_outline_animation(true));
    info!("Waiting for first NTP sync");
    let _clock_sync_tick = clock_sync.wait_for_tick().await;
    info!("First NTP sync complete");

    let led4_ref = &led4;
    run_clock_ui(
        &clock_sync,
        &mut *button_watch6,
        &mut timezone_flash_block,
        |clock_ui_event| async move {
            match clock_ui_event {
                ClockUiEvent::RenderHoursMinutes { hours, minutes } => {
                    led4_ref.write_text(hours_minutes_text(hours, minutes), BlinkState::Solid);
                }
                ClockUiEvent::RenderMinutesSeconds { minutes, seconds } => {
                    led4_ref.write_text(minutes_seconds_text(minutes, seconds), BlinkState::Solid);
                }
                ClockUiEvent::RenderHoursMinutesEdit { hours, minutes } => {
                    led4_ref.write_text(
                        hours_minutes_text(hours, minutes),
                        BlinkState::BlinkingAndOn,
                    );
                }
            }
            Ok(())
        },
    )
    .await
}

fn hours_minutes_text(hours: u8, minutes: u8) -> [char; 4] {
    [
        tens_hours(hours),
        ones_digit(hours),
        tens_digit(minutes),
        ones_digit(minutes),
    ]
}

fn minutes_seconds_text(minutes: u8, seconds: u8) -> [char; 4] {
    [
        tens_digit(minutes),
        ones_digit(minutes),
        tens_digit(seconds),
        ones_digit(seconds),
    ]
}

#[inline]
#[expect(
    clippy::arithmetic_side_effects,
    clippy::integer_division_remainder_used,
    reason = "Value < 60 ensures division is safe"
)]
const fn tens_digit(value: u8) -> char {
    ((value / 10) + b'0') as char
}

#[inline]
const fn tens_hours(value: u8) -> char {
    if value >= 10 {
        '1'
    } else {
        ' '
    }
}

#[inline]
#[expect(
    clippy::arithmetic_side_effects,
    clippy::integer_division_remainder_used,
    reason = "Value < 60 ensures division is safe"
)]
const fn ones_digit(value: u8) -> char {
    ((value % 10) + b'0') as char
}
