#![allow(missing_docs)]
//! Wi-Fi enabled clock that visualizes time with two servos.
//!
//! This example combines the `WifiAuto` captive-portal workflow with a servo-based
//! display. Because the servos are mounted reversed, the left servo shows minutes/seconds
//! and the right servo shows hours/minutes with 180° reflections applied.

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

use core::convert::{Infallible, TryFrom};
use defmt::info;
use defmt_rtt as _;
use device_envoy::button::{PressDuration, PressedTo};
use device_envoy::button_watch;
use device_envoy::clock_sync::{
    ClockSync, ClockSyncStatic, ONE_DAY, ONE_MINUTE, ONE_SECOND, h12_m_s,
};
use device_envoy::flash_array::FlashArray;
use device_envoy::servo_player::{AtEnd, combine, linear, servo_player};
use device_envoy::wifi_auto::fields::{TimezoneField, TimezoneFieldStatic};
use device_envoy::wifi_auto::{WifiAuto, WifiAutoEvent};
use device_envoy::{Error, Result};
use embassy_executor::Spawner;
use embassy_futures::select::{Either, select};
use embassy_time::{Duration, Timer};
use panic_probe as _;

const FAST_MODE_SPEED: f32 = 720.0;

button_watch! {
    ButtonWatch13 {
        pin: PIN_13,
    }
}

// Define two typed servo players at module scope
servo_player! {
    BottomServoPlayer {
        pin: PIN_11,
        max_steps: 30,
    }
}

servo_player! {
    TopServoPlayer {
        pin: PIN_12,
        max_steps: 30,
    }
}

#[embassy_executor::main]
pub async fn main(spawner: Spawner) -> ! {
    let err = inner_main(spawner).await.unwrap_err();
    core::panic!("{err}");
}

async fn inner_main(spawner: Spawner) -> Result<Infallible> {
    info!("Starting Wi-Fi servo clock (WifiAuto)");
    let p = embassy_rp::init(Default::default());

    // Use two blocks of flash storage: Wi-Fi credentials + timezone
    let [wifi_credentials_flash_block, timezone_flash_block] = FlashArray::<2>::new(p.FLASH)?;

    // Define HTML to ask for timezone on the captive portal.
    static TIMEZONE_FIELD_STATIC: TimezoneFieldStatic = TimezoneField::new_static();
    let timezone_field = TimezoneField::new(&TIMEZONE_FIELD_STATIC, timezone_flash_block);

    // Set up Wifi via a captive portal. The button pin is used to reset stored credentials.
    let wifi_auto = WifiAuto::new(
        p.PIN_23,  // CYW43 power
        p.PIN_24,  // CYW43 clock
        p.PIN_25,  // CYW43 chip select
        p.PIN_29,  // CYW43 data pin
        p.PIO0,    // CYW43 PIO interface
        p.DMA_CH0, // CYW43 DMA channel
        wifi_credentials_flash_block,
        p.PIN_13, // Reset button pin
        PressedTo::Ground,
        "PicoServoClock", // Captive-portal SSID
        [timezone_field],
        spawner,
    )?;

    // Configure two servos for the display.
    let bottom_servo = BottomServoPlayer::new(p.PIN_11, p.PWM_SLICE5, spawner)?;
    let top_servo = TopServoPlayer::new(p.PIN_12, p.PWM_SLICE6, spawner)?;
    let servo_display = ServoClockDisplay::new(bottom_servo, top_servo);

    // Connect Wi-Fi, using the servos for status indications.
    let servo_display_ref = &servo_display;
    let (stack, button) = wifi_auto
        .connect(|event| {
            let servo_display_ref = servo_display_ref;
            async move {
                match event {
                    WifiAutoEvent::CaptivePortalReady => {
                        servo_display_ref.show_portal_ready().await;
                    }
                    WifiAutoEvent::Connecting { .. } => servo_display_ref.show_connecting().await,
                    WifiAutoEvent::ConnectionFailed => {
                        // No-op; portal remains visible on failure.
                    }
                }
                Ok(())
            }
        })
        .await?;

    info!("WiFi connected");

    // Convert the Button from WifiAuto into a ButtonWatch for background monitoring
    let button_watch13 = ButtonWatch13::from_button(button, spawner)?;

    // Read the timezone offset, an extra field that WiFi portal saved to flash.
    let offset_minutes = timezone_field
        .offset_minutes()?
        .ok_or(Error::MissingCustomWifiAutoField)?;

    // Create a ClockSync device that knows its timezone offset.
    static CLOCK_SYNC_STATIC: ClockSyncStatic = ClockSync::new_static();
    let clock_sync = ClockSync::new(
        &CLOCK_SYNC_STATIC,
        stack,
        offset_minutes,
        Some(ONE_MINUTE),
        spawner,
    );

    // Start in HH:MM mode
    let mut state = State::HoursMinutes { speed: 1.0 };
    loop {
        state = match state {
            State::HoursMinutes { speed } => {
                state
                    .execute_hours_minutes(speed, &clock_sync, button_watch13, &servo_display)
                    .await?
            }
            State::MinutesSeconds => {
                state
                    .execute_minutes_seconds(&clock_sync, button_watch13, &servo_display)
                    .await?
            }
            State::EditOffset => {
                state
                    .execute_edit_offset(
                        &clock_sync,
                        button_watch13,
                        &timezone_field,
                        &servo_display,
                    )
                    .await?
            }
        };
    }
}

// State machine for servo clock display modes and transitions.

/// Display states for the servo clock.
#[derive(Debug, defmt::Format, Clone, Copy, PartialEq)]
pub enum State {
    HoursMinutes { speed: f32 },
    MinutesSeconds,
    EditOffset,
}

impl State {
    async fn execute_hours_minutes(
        self,
        speed: f32,
        clock_sync: &ClockSync,
        button_watch13: &ButtonWatch13,
        servo_display: &ServoClockDisplay,
    ) -> Result<Self> {
        clock_sync.set_speed(speed).await;
        let (hours, minutes, _) = h12_m_s(&clock_sync.now_local());
        servo_display.show_hours_minutes(hours, minutes).await;
        clock_sync.set_tick_interval(Some(ONE_MINUTE)).await;
        loop {
            match select(
                button_watch13.wait_for_press_duration(),
                clock_sync.wait_for_tick(),
            )
            .await
            {
                // Button pushes
                Either::First(press_duration) => match (press_duration, speed.to_bits()) {
                    (PressDuration::Short, bits) if bits == 1.0f32.to_bits() => {
                        return Ok(Self::MinutesSeconds);
                    }
                    (PressDuration::Short, _) => {
                        return Ok(Self::HoursMinutes { speed: 1.0 });
                    }
                    (PressDuration::Long, _) => {
                        return Ok(Self::EditOffset);
                    }
                },
                // Clock tick
                Either::Second(tick) => {
                    let (hours, minutes, _) = h12_m_s(&tick.local_time);
                    servo_display.show_hours_minutes(hours, minutes).await;
                }
            }
        }
    }

    async fn execute_minutes_seconds(
        self,
        clock_sync: &ClockSync,
        button_watch13: &ButtonWatch13,
        servo_display: &ServoClockDisplay,
    ) -> Result<Self> {
        clock_sync.set_speed(1.0).await;
        let (_, minutes, seconds) = h12_m_s(&clock_sync.now_local());
        servo_display.show_minutes_seconds(minutes, seconds).await;
        clock_sync.set_tick_interval(Some(ONE_SECOND)).await;
        loop {
            match select(
                button_watch13.wait_for_press_duration(),
                clock_sync.wait_for_tick(),
            )
            .await
            {
                // Button pushes
                Either::First(PressDuration::Short) => {
                    return Ok(Self::HoursMinutes {
                        speed: FAST_MODE_SPEED,
                    });
                }
                Either::First(PressDuration::Long) => {
                    return Ok(Self::EditOffset);
                }
                // Clock tick
                Either::Second(tick) => {
                    let (_, minutes, seconds) = h12_m_s(&tick.local_time);
                    servo_display.show_minutes_seconds(minutes, seconds).await;
                }
            }
        }
    }

    async fn execute_edit_offset(
        self,
        clock_sync: &ClockSync,
        button_watch13: &ButtonWatch13,
        timezone_field: &TimezoneField,
        servo_display: &ServoClockDisplay,
    ) -> Result<Self> {
        info!("Entering edit offset mode");
        clock_sync.set_speed(1.0).await;

        // Show current hours and minutes
        let (hours, minutes, _) = h12_m_s(&clock_sync.now_local());
        servo_display
            .show_hours_minutes_indicator(hours, minutes)
            .await;
        // Add a gentle wiggle on the bottom servo to signal edit mode.
        const WIGGLE: [(u16, Duration); 2] = [
            (80, Duration::from_millis(250)),
            (100, Duration::from_millis(250)),
        ];
        servo_display.bottom.animate(WIGGLE, AtEnd::Loop);

        // Get the current offset minutes from clock (source of truth)
        let mut offset_minutes = clock_sync.offset_minutes();
        info!("Current offset: {} minutes", offset_minutes);

        clock_sync.set_tick_interval(None).await; // Disable ticks in edit mode
        loop {
            info!("Waiting for button press in edit mode");
            match button_watch13.wait_for_press_duration().await {
                PressDuration::Short => {
                    info!("Short press detected - incrementing offset");
                    // Increment the offset by 1 hour
                    offset_minutes += 60;
                    const ONE_DAY_MINUTES: i32 = ONE_DAY.as_secs() as i32 / 60;
                    if offset_minutes >= ONE_DAY_MINUTES {
                        offset_minutes -= ONE_DAY_MINUTES;
                    }
                    clock_sync.set_offset_minutes(offset_minutes).await;
                    info!("New offset: {} minutes", offset_minutes);

                    // Update display (atomic already updated, can use now_local)
                    let (hours, minutes, _) = h12_m_s(&clock_sync.now_local());
                    info!(
                        "Updated time after offset change: {:02}:{:02}",
                        hours, minutes
                    );
                    servo_display
                        .show_hours_minutes_indicator(hours, minutes)
                        .await;
                    servo_display.bottom.animate(WIGGLE, AtEnd::Loop);
                }
                PressDuration::Long => {
                    info!("Long press detected - saving and exiting edit mode");
                    // Save to flash and exit edit mode
                    timezone_field.set_offset_minutes(offset_minutes)?;
                    info!("Offset saved to flash: {} minutes", offset_minutes);
                    return Ok(Self::HoursMinutes { speed: 1.0 });
                }
            }
        }
    }
}

struct ServoClockDisplay {
    bottom: &'static BottomServoPlayer,
    top: &'static TopServoPlayer,
}

impl ServoClockDisplay {
    fn new(bottom: &'static BottomServoPlayer, top: &'static TopServoPlayer) -> Self {
        Self { bottom, top }
    }

    async fn show_portal_ready(&self) {
        self.bottom.set_degrees(90);
        self.top.set_degrees(90);
    }

    async fn show_connecting(&self) {
        // Animate both servos in complementary two-phase sweeps.
        const FIVE_SECONDS: Duration = Duration::from_secs(5);
        const PHASE1: [(u16, Duration); 10] = linear(180 - 18, 0, FIVE_SECONDS);
        const PHASE2: [(u16, Duration); 2] = linear(0, 180, FIVE_SECONDS);
        self.top.animate(combine!(PHASE1, PHASE2), AtEnd::Loop);
        self.bottom.animate(combine!(PHASE2, PHASE1), AtEnd::Loop);
    }

    async fn show_hours_minutes(&self, hours: u8, minutes: u8) {
        let left_angle = hours_to_degrees(hours);
        let right_angle = sixty_to_degrees(minutes);
        self.set_angles(left_angle, right_angle).await;
        Timer::after(Duration::from_millis(500)).await;
        self.bottom.relax();
        self.top.relax();
    }

    async fn show_hours_minutes_indicator(&self, hours: u8, minutes: u8) {
        let left_angle = hours_to_degrees(hours);
        let right_angle = sixty_to_degrees(minutes);
        self.set_angles(left_angle, right_angle).await;
        Timer::after(Duration::from_millis(500)).await;
        self.bottom.relax();
        self.top.relax();
    }

    async fn show_minutes_seconds(&self, minutes: u8, seconds: u8) {
        let left_angle = sixty_to_degrees(minutes);
        let right_angle = sixty_to_degrees(seconds);
        self.set_angles(left_angle, right_angle).await;
    }

    async fn set_angles(&self, left_degrees: i32, right_degrees: i32) {
        // Swap servos and reflect angles for physical orientation.
        let physical_left = reflect_degrees(right_degrees);
        let physical_right = reflect_degrees(left_degrees);
        let left_angle =
            u16::try_from(physical_left).expect("servo angles must be between 0 and 180 degrees");
        let right_angle =
            u16::try_from(physical_right).expect("servo angles must be between 0 and 180 degrees");
        self.bottom.set_degrees(left_angle);
        self.top.set_degrees(right_angle);
    }
}

#[inline]
fn hours_to_degrees(hours: u8) -> i32 {
    assert!((1..=12).contains(&hours));
    let normalized_hour = hours % 12;
    i32::from(normalized_hour) * 180 / 12
}

#[inline]
fn sixty_to_degrees(value: u8) -> i32 {
    assert!(value < 60);
    i32::from(value) * 180 / 60
}

#[inline]
fn reflect_degrees(degrees: i32) -> i32 {
    assert!((0..=180).contains(&degrees));
    180 - degrees
}
