//! Alarm support

use super::super::{BitFlags, Ds323x, Error, Hours, Register};
use super::{decimal_to_packed_bcd, hours_to_register};
use crate::ds323x::{NaiveTime, Timelike};
use interface::{ReadData, WriteData};

/// Parameters for setting Alarm1 on a day of the month
///
/// Depending on the matching strategy, some fields may not be relevant. In this
/// case, invalid values are ignored and basic values are used instead to
/// configure the alarm:
/// - second, minute and hour : 0
/// - day: 1
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct DayAlarm1 {
    /// Day of the month [1-31]
    pub day: u8,
    /// Hour
    pub hour: Hours,
    /// Minute [0-59]
    pub minute: u8,
    /// Second [0-59]
    pub second: u8,
}

/// Parameters for setting Alarm1 on a weekday
///
/// Depending on the matching strategy, some fields may not be relevant. In this
/// case, invalid values are ignored and basic values are used instead to
/// configure the alarm:
/// - second, minute and hour : 0
/// - weekday: 1
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct WeekdayAlarm1 {
    /// Weekday [1-7]
    pub weekday: u8,
    /// Hour
    pub hour: Hours,
    /// Minute [0-59]
    pub minute: u8,
    /// Second [0-59]
    pub second: u8,
}

/// Alarm1 trigger rate
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Alarm1Matching {
    /// Alarm once per second.
    OncePerSecond,
    /// Alarm when seconds match.
    SecondsMatch,
    /// Alarm when minutes and seconds match.
    MinutesAndSecondsMatch,
    /// Alarm when hours, minutes and seconds match.
    HoursMinutesAndSecondsMatch,
    /// Alarm when date/weekday, hours, minutes and seconds match.
    AllMatch,
}

/// Parameters for setting Alarm2 on a day of the month
///
/// Depending on the matching strategy, some fields may not be relevant. In this
/// case, invalid values are ignored and basic values are used instead to
/// configure the alarm:
/// - minute and hour : 0
/// - day: 1
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct DayAlarm2 {
    /// Day of month [1-31]
    pub day: u8,
    /// Hour
    pub hour: Hours,
    /// Minute [0-59]
    pub minute: u8,
}

/// Parameters for setting Alarm2 on a weekday
///
/// Depending on the matching strategy, some fields may not be relevant. In this
/// case, invalid values are ignored and basic values are used instead to
/// configure the alarm:
/// - minute and hour : 0
/// - weekday: 1
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct WeekdayAlarm2 {
    /// Weekday [1-7]
    pub weekday: u8,
    /// Hour
    pub hour: Hours,
    /// Minute [0-59]
    pub minute: u8,
}

/// Alarm2 trigger rate
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Alarm2Matching {
    /// Alarm once per minute. (00 seconds of every minute)
    OncePerMinute,
    /// Alarm when minutes match.
    MinutesMatch,
    /// Alarm when hours and minutes match.
    HoursAndMinutesMatch,
    /// Alarm when date/weekday, hours and minutes match.
    AllMatch,
}

fn get_matching_mask_alarm1(matching: Alarm1Matching) -> [u8; 4] {
    const AM: u8 = BitFlags::ALARM_MATCH;
    match matching {
        Alarm1Matching::OncePerSecond => [AM, AM, AM, AM],
        Alarm1Matching::SecondsMatch => [0, AM, AM, AM],
        Alarm1Matching::MinutesAndSecondsMatch => [0, 0, AM, AM],
        Alarm1Matching::HoursMinutesAndSecondsMatch => [0, 0, 0, AM],
        Alarm1Matching::AllMatch => [0, 0, 0, 0],
    }
}

fn get_matching_mask_alarm2(matching: Alarm2Matching) -> [u8; 3] {
    const AM: u8 = BitFlags::ALARM_MATCH;
    match matching {
        Alarm2Matching::OncePerMinute => [AM, AM, AM],
        Alarm2Matching::MinutesMatch => [0, AM, AM],
        Alarm2Matching::HoursAndMinutesMatch => [0, 0, AM],
        Alarm2Matching::AllMatch => [0, 0, 0],
    }
}

/// Test if hours value is valid
///
/// Will return true if valid, false if not
fn is_hour_valid(hours: Hours) -> bool {
    match hours {
        Hours::H24(h) if h > 23 => true,
        Hours::AM(h) if h < 1 || h > 12 => true,
        Hours::PM(h) if h < 1 || h > 12 => true,
        _ => false,
    }
}

/// Relax the hour value by changing an incorrect value by 0
fn relax_hour(hours: Hours) -> Hours {
    match hours {
        Hours::H24(h) if h > 23 => Hours::H24(0),
        Hours::H24(h) => Hours::H24(h),
        Hours::AM(h) if h < 1 || h > 12 => Hours::AM(1),
        Hours::AM(h) => Hours::AM(h),
        Hours::PM(h) if h < 1 || h > 12 => Hours::PM(1),
        Hours::PM(h) => Hours::PM(h),
    }
}

impl<DI, IC, CommE, PinE> Ds323x<DI, IC>
where
    DI: ReadData<Error = Error<CommE, PinE>> + WriteData<Error = Error<CommE, PinE>>,
{
    /// Set Alarm1 for day of the month.
    ///
    /// Will return an `Error::InvalidInputData` if any of the used parameters
    /// (depending on the matching startegy) is out of range. Any unused
    /// parameter is set to basic valid value:
    /// - second, minute, hour: 0
    /// - day: 1
    pub fn set_alarm1_day(
        &mut self,
        when: DayAlarm1,
        matching: Alarm1Matching,
    ) -> Result<(), Error<CommE, PinE>> {
        let day_invalid = when.day < 1 || when.day > 31;
        let hour_invalid = is_hour_valid(when.hour);
        let minute_invalid = when.minute > 59;
        let second_invalid = when.second > 59;

        let day = if day_invalid { 1 } else { when.day };
        let hour = relax_hour(when.hour);
        let minute = if minute_invalid { 0 } else { when.minute };

        if (matching == Alarm1Matching::AllMatch && (day_invalid || hour_invalid))
            || (hour_invalid && matching == Alarm1Matching::HoursMinutesAndSecondsMatch)
            || ((matching != Alarm1Matching::SecondsMatch
                && matching != Alarm1Matching::OncePerSecond)
                && minute_invalid)
            || second_invalid
        {
            return Err(Error::InvalidInputData);
        }

        let match_mask = get_matching_mask_alarm1(matching);
        let mut data = [
            Register::ALARM1_SECONDS,
            decimal_to_packed_bcd(when.second) | match_mask[0],
            decimal_to_packed_bcd(minute) | match_mask[1],
            hours_to_register(hour)? | match_mask[2],
            decimal_to_packed_bcd(day) | match_mask[3],
        ];
        self.iface.write_data(&mut data)
    }

    /// Set Alarm1 for a time (fires when hours, minutes and seconds match).
    ///
    /// Will return an `Error::InvalidInputData` if any of the parameters is out of range.
    /// day is not used by the matching strategy but is set to 1.
    pub fn set_alarm1_hms(&mut self, when: NaiveTime) -> Result<(), Error<CommE, PinE>> {
        let alarm = DayAlarm1 {
            day: 1,
            hour: Hours::H24(when.hour() as u8),
            minute: when.minute() as u8,
            second: when.second() as u8,
        };
        self.set_alarm1_day(alarm, Alarm1Matching::HoursMinutesAndSecondsMatch)
    }

    /// Set Alarm1 for weekday.
    ///
    /// Will return an `Error::InvalidInputData` if any of the used parameters
    /// (depending on the matching startegy) is out of range. Any unused
    /// parameter is set to basic valid value:
    /// - second, minute, hour: 0
    /// - weekday: 1
    pub fn set_alarm1_weekday(
        &mut self,
        when: WeekdayAlarm1,
        matching: Alarm1Matching,
    ) -> Result<(), Error<CommE, PinE>> {
        let weekday_invalid = when.weekday < 1 || when.weekday > 7;
        let hour_invalid = is_hour_valid(when.hour);
        let minute_invalid = when.minute > 59;
        let second_invalid = when.second > 59;

        let weekday = if weekday_invalid { 1 } else { when.weekday };
        let hour = relax_hour(when.hour);
        let minute = if minute_invalid { 0 } else { when.minute };
        let second = if second_invalid { 0 } else { when.second };

        if ((hour_invalid || weekday_invalid) && matching == Alarm1Matching::AllMatch)
            || (hour_invalid && matching == Alarm1Matching::HoursMinutesAndSecondsMatch)
            || (minute_invalid
                && (matching != Alarm1Matching::OncePerSecond
                    && matching != Alarm1Matching::SecondsMatch))
            || (second_invalid && matching != Alarm1Matching::OncePerSecond)
        {
            return Err(Error::InvalidInputData);
        }
        let match_mask = get_matching_mask_alarm1(matching);
        let mut data = [
            Register::ALARM1_SECONDS,
            decimal_to_packed_bcd(second) | match_mask[0],
            decimal_to_packed_bcd(minute) | match_mask[1],
            hours_to_register(hour)? | match_mask[2],
            decimal_to_packed_bcd(weekday) | match_mask[3] | BitFlags::WEEKDAY,
        ];
        self.iface.write_data(&mut data)
    }

    /// Set Alarm2 for date (day of month).
    ///
    /// Will return an `Error::InvalidInputData` if any of the used parameters
    /// (depending on the matching startegy) is out of range. Any unused
    /// parameter is set to basic valid value:
    /// - minute, hour: 0
    /// - day: 1
    pub fn set_alarm2_day(
        &mut self,
        when: DayAlarm2,
        matching: Alarm2Matching,
    ) -> Result<(), Error<CommE, PinE>> {
        let day_invalid = when.day < 1 || when.day > 31;
        let hour_invalid = is_hour_valid(when.hour);
        let minute_invalid = when.minute > 59;

        let day = if day_invalid { 1 } else { when.day };
        let hour = relax_hour(when.hour);
        let minute = if minute_invalid { 0 } else { when.minute };

        if ((day_invalid || hour_invalid) && matching == Alarm2Matching::AllMatch)
            || (hour_invalid && matching == Alarm2Matching::HoursAndMinutesMatch)
            || (matching != Alarm2Matching::OncePerMinute && minute_invalid)
        {
            return Err(Error::InvalidInputData);
        }

        let match_mask = get_matching_mask_alarm2(matching);
        let mut data = [
            Register::ALARM2_MINUTES,
            decimal_to_packed_bcd(minute) | match_mask[0],
            hours_to_register(hour)? | match_mask[1],
            decimal_to_packed_bcd(day) | match_mask[2],
        ];
        self.iface.write_data(&mut data)
    }

    /// Set Alarm2 for a time (fires when hours, minutes and seconds match).
    ///
    /// Will return an `Error::InvalidInputData` if any of the parameters is out of range.
    /// day is not used by the matching strategy but is set to 1.
    pub fn set_alarm2_hm(&mut self, when: NaiveTime) -> Result<(), Error<CommE, PinE>> {
        let alarm = DayAlarm2 {
            day: 1,
            hour: Hours::H24(when.hour() as u8),
            minute: when.minute() as u8,
        };
        self.set_alarm2_day(alarm, Alarm2Matching::HoursAndMinutesMatch)
    }

    /// Set Alarm2 for weekday.
    ///
    /// Will return an `Error::InvalidInputData` if any of the used parameters
    /// (depending on the matching startegy) is out of range. Any unused
    /// parameter is set to basic valid value:
    /// - minute, hour: 0
    /// - weekday: 1
    pub fn set_alarm2_weekday(
        &mut self,
        when: WeekdayAlarm2,
        matching: Alarm2Matching,
    ) -> Result<(), Error<CommE, PinE>> {
        let weekday_invalid = when.weekday < 1 || when.weekday > 7;
        let hour_invalid = is_hour_valid(when.hour);
        let minute_invalid = when.minute > 59;

        let weekday = if weekday_invalid { 1 } else { when.weekday };
        let hour = relax_hour(when.hour);
        let minute = if minute_invalid { 0 } else { when.minute };

        if (matching == Alarm2Matching::AllMatch && (weekday_invalid || hour_invalid))
            || (matching == Alarm2Matching::HoursAndMinutesMatch && hour_invalid)
            || (minute_invalid && matching != Alarm2Matching::OncePerMinute)
        {
            return Err(Error::InvalidInputData);
        }
        let match_mask = get_matching_mask_alarm2(matching);
        let mut data = [
            Register::ALARM2_MINUTES,
            decimal_to_packed_bcd(minute) | match_mask[0],
            hours_to_register(hour)? | match_mask[1],
            decimal_to_packed_bcd(weekday) | match_mask[2] | BitFlags::WEEKDAY,
        ];
        self.iface.write_data(&mut data)
    }
}