mod filter;

use embassy_hal_common::{into_ref, Peripheral, PeripheralRef};

pub use self::filter::DateTimeFilter;

#[cfg_attr(feature = "chrono", path = "datetime_chrono.rs")]
#[cfg_attr(not(feature = "chrono"), path = "datetime_no_deps.rs")]
mod datetime;

pub use self::datetime::{DateTime, DayOfWeek, Error as DateTimeError};
use crate::clocks::clk_rtc_freq;

/// A reference to the real time clock of the system
pub struct RealTimeClock<'d, T: Instance> {
    inner: PeripheralRef<'d, T>,
}

impl<'d, T: Instance> RealTimeClock<'d, T> {
    /// Create a new instance of the real time clock, with the given date as an initial value.
    ///
    /// # Errors
    ///
    /// Will return `RtcError::InvalidDateTime` if the datetime is not a valid range.
    pub fn new(inner: impl Peripheral<P = T> + 'd, initial_date: DateTime) -> Result<Self, RtcError> {
        into_ref!(inner);

        // Set the RTC divider
        unsafe {
            inner
                .regs()
                .clkdiv_m1()
                .write(|w| w.set_clkdiv_m1(clk_rtc_freq() as u16 - 1))
        };

        let mut result = Self { inner };
        result.set_leap_year_check(true); // should be on by default, make sure this is the case.
        result.set_datetime(initial_date)?;
        Ok(result)
    }

    /// Enable or disable the leap year check. The rp2040 chip will always add a Feb 29th on every year that is divisable by 4, but this may be incorrect (e.g. on century years). This function allows you to disable this check.
    ///
    /// Leap year checking is enabled by default.
    pub fn set_leap_year_check(&mut self, leap_year_check_enabled: bool) {
        unsafe {
            self.inner
                .regs()
                .ctrl()
                .modify(|w| w.set_force_notleapyear(!leap_year_check_enabled))
        };
    }

    /// Checks to see if this RealTimeClock is running
    pub fn is_running(&self) -> bool {
        unsafe { self.inner.regs().ctrl().read().rtc_active() }
    }

    /// Set the datetime to a new value.
    ///
    /// # Errors
    ///
    /// Will return `RtcError::InvalidDateTime` if the datetime is not a valid range.
    pub fn set_datetime(&mut self, t: DateTime) -> Result<(), RtcError> {
        self::datetime::validate_datetime(&t).map_err(RtcError::InvalidDateTime)?;

        // disable RTC while we configure it
        unsafe {
            self.inner.regs().ctrl().modify(|w| w.set_rtc_enable(false));
            while self.inner.regs().ctrl().read().rtc_active() {
                core::hint::spin_loop();
            }

            self.inner.regs().setup_0().write(|w| {
                self::datetime::write_setup_0(&t, w);
            });
            self.inner.regs().setup_1().write(|w| {
                self::datetime::write_setup_1(&t, w);
            });

            // Load the new datetime and re-enable RTC
            self.inner.regs().ctrl().write(|w| w.set_load(true));
            self.inner.regs().ctrl().write(|w| w.set_rtc_enable(true));
            while !self.inner.regs().ctrl().read().rtc_active() {
                core::hint::spin_loop();
            }
        }
        Ok(())
    }

    /// Return the current datetime.
    ///
    /// # Errors
    ///
    /// Will return an `RtcError::InvalidDateTime` if the stored value in the system is not a valid [`DayOfWeek`].
    pub fn now(&self) -> Result<DateTime, RtcError> {
        if !self.is_running() {
            return Err(RtcError::NotRunning);
        }

        let rtc_0 = unsafe { self.inner.regs().rtc_0().read() };
        let rtc_1 = unsafe { self.inner.regs().rtc_1().read() };

        self::datetime::datetime_from_registers(rtc_0, rtc_1).map_err(RtcError::InvalidDateTime)
    }

    /// Disable the alarm that was scheduled with [`schedule_alarm`].
    ///
    /// [`schedule_alarm`]: #method.schedule_alarm
    pub fn disable_alarm(&mut self) {
        unsafe {
            self.inner.regs().irq_setup_0().modify(|s| s.set_match_ena(false));

            while self.inner.regs().irq_setup_0().read().match_active() {
                core::hint::spin_loop();
            }
        }
    }

    /// Schedule an alarm. The `filter` determines at which point in time this alarm is set.
    ///
    /// Keep in mind that the filter only triggers on the specified time. If you want to schedule this alarm every minute, you have to call:
    /// ```no_run
    /// # #[cfg(feature = "chrono")]
    /// # fn main() { }
    /// # #[cfg(not(feature = "chrono"))]
    /// # fn main() {
    /// # use embassy_rp::rtc::{RealTimeClock, DateTimeFilter};
    /// # let mut real_time_clock: RealTimeClock = unsafe { core::mem::zeroed() };
    /// let now = real_time_clock.now().unwrap();
    /// real_time_clock.schedule_alarm(
    ///     DateTimeFilter::default()
    ///         .minute(if now.minute == 59 { 0 } else { now.minute + 1 })
    /// );
    /// # }
    /// ```
    pub fn schedule_alarm(&mut self, filter: DateTimeFilter) {
        self.disable_alarm();

        unsafe {
            self.inner.regs().irq_setup_0().write(|w| {
                filter.write_setup_0(w);
            });
            self.inner.regs().irq_setup_1().write(|w| {
                filter.write_setup_1(w);
            });

            self.inner.regs().inte().modify(|w| w.set_rtc(true));

            // Set the enable bit and check if it is set
            self.inner.regs().irq_setup_0().modify(|w| w.set_match_ena(true));
            while !self.inner.regs().irq_setup_0().read().match_active() {
                core::hint::spin_loop();
            }
        }
    }

    /// Clear the interrupt. This should be called every time the `RTC_IRQ` interrupt is triggered,
    /// or the next [`schedule_alarm`] will never fire.
    ///
    /// [`schedule_alarm`]: #method.schedule_alarm
    pub fn clear_interrupt(&mut self) {
        self.disable_alarm();
    }
}

/// Errors that can occur on methods on [RealTimeClock]
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum RtcError {
    /// An invalid DateTime was given or stored on the hardware.
    InvalidDateTime(DateTimeError),

    /// The RTC clock is not running
    NotRunning,
}

mod sealed {
    pub trait Instance {
        fn regs(&self) -> crate::pac::rtc::Rtc;
    }
}

pub trait Instance: sealed::Instance {}

impl sealed::Instance for crate::peripherals::RTC {
    fn regs(&self) -> crate::pac::rtc::Rtc {
        crate::pac::RTC
    }
}
impl Instance for crate::peripherals::RTC {}