use chrono::{Datelike, Duration, NaiveDate, Weekday};
use serde::{Deserialize, Serialize};
use std::collections::HashSet;

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct Repetition {
    pub frequency: Frequency,
    pub removed_occurences: HashSet<usize>,
    pub until: Option<NaiveDate>,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum Frequency {
    Daily { period: u32 },
    Monthly { day: DayOfMonth },
    Yearly,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum DayOfMonth {
    Day { day: u8 },
    Weekday { weekday: Weekday },
}

pub fn validate_period(str: &str) -> Result<u32, String> {
    let n = str
        .parse::<u32>()
        .map_err(|_| format!("{} n’est pas une période valide.", str))?;
    if n == 0 {
        Err("La periode doit être positive.".to_string())
    } else {
        Ok(n)
    }
}

pub fn validate_day(str: &str) -> Result<u8, String> {
    let n = str
        .parse::<u8>()
        .map_err(|_| format!("« {} » n’est pas un jour valide.", str))?;
    if (1..=31).contains(&n) {
        Ok(n)
    } else {
        Err("Le jour devrait se situer entre le 1er et le 31 du mois.".to_string())
    }
}

impl Repetition {
    pub fn between(&self, event: NaiveDate, start: NaiveDate, end: NaiveDate) -> Vec<NaiveDate> {
        let repeat = |mut date, next: Box<dyn Fn(NaiveDate) -> NaiveDate>| {
            let mut repetitions = vec![];
            let mut iteration: usize = 0;
            let end = self.until.map(|u| u.min(end)).unwrap_or(end);
            while date <= end {
                if date >= event {
                    if date >= start && !self.removed_occurences.contains(&iteration) {
                        repetitions.push(date)
                    }
                    iteration += 1
                }
                date = next(date);
            }
            repetitions
        };

        match self.frequency {
            Frequency::Daily { period } => {
                let duration = Duration::days(period as i64);
                repeat(event, Box::new(|d| d + duration))
            }
            Frequency::Monthly {
                day: DayOfMonth::Day { day },
            } => match event.with_day(day as u32) {
                Some(first_repetition) => repeat(first_repetition, Box::new(next_month)),
                None => vec![],
            },
            Frequency::Monthly {
                day: DayOfMonth::Weekday { weekday },
            } => repeat(
                first_weekday_of_month(event, weekday),
                Box::new(|d| first_weekday_of_month(next_month(d), weekday)),
            ),
            Frequency::Yearly => repeat(
                // TODO: error handling
                NaiveDate::from_ymd_opt(event.year(), event.month(), event.day()).unwrap(),
                Box::new(|d| NaiveDate::from_ymd_opt(d.year() + 1, d.month(), d.day()).unwrap()),
            ),
        }
    }

    pub fn occurence_index(&self, event: NaiveDate, date: NaiveDate) -> Option<usize> {
        let mut without_removed_occurences = self.clone();
        without_removed_occurences.removed_occurences = HashSet::new();
        without_removed_occurences
            .between(event, event, date)
            .iter()
            .position(|d| d == &date)
    }
}

fn first_weekday_of_month(date: NaiveDate, weekday: Weekday) -> NaiveDate {
    // TODO: error handling
    NaiveDate::from_weekday_of_month_opt(date.year(), date.month(), weekday, 1).unwrap()
}

fn next_month(date: NaiveDate) -> NaiveDate {
    // TODO: error handling
    if date.month() == 12 {
        NaiveDate::from_ymd_opt(date.year() + 1, 1, date.day()).unwrap()
    } else {
        NaiveDate::from_ymd_opt(date.year(), date.month() + 1, date.day()).unwrap()
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn every_day_event_before() {
        let repetition = from_freq(Frequency::Daily { period: 1 });
        assert_eq!(
            repetition.between(d(2022, 6, 1), d(2022, 7, 1), d(2022, 8, 31)),
            d(2022, 7, 1)
                .iter_days()
                .take(62)
                .collect::<Vec<NaiveDate>>()
        )
    }

    #[test]
    fn every_day_event_between() {
        let repetition = from_freq(Frequency::Daily { period: 1 });
        assert_eq!(
            repetition.between(d(2022, 8, 10), d(2022, 7, 1), d(2022, 8, 31)),
            d(2022, 8, 10)
                .iter_days()
                .take(22)
                .collect::<Vec<NaiveDate>>()
        )
    }

    #[test]
    fn every_day_event_after() {
        let repetition = from_freq(Frequency::Daily { period: 1 });
        assert!(repetition
            .between(d(2022, 9, 1), d(2022, 7, 1), d(2022, 8, 31))
            .is_empty())
    }

    #[test]
    fn every_three_days() {
        let repetition = from_freq(Frequency::Daily { period: 3 });
        assert_eq!(
            repetition.between(d(2022, 2, 16), d(2022, 2, 21), d(2022, 3, 6)),
            vec!(
                d(2022, 2, 22),
                d(2022, 2, 25),
                d(2022, 2, 28),
                d(2022, 3, 3),
                d(2022, 3, 6)
            )
        )
    }

    #[test]
    fn day_of_month() {
        let repetition = from_freq(Frequency::Monthly {
            day: DayOfMonth::Day { day: 8 },
        });
        assert_eq!(
            repetition.between(d(2022, 2, 7), d(2022, 1, 1), d(2022, 4, 7)),
            vec!(d(2022, 2, 8), d(2022, 3, 8))
        )
    }

    #[test]
    fn weekday_of_month() {
        let repetition = from_freq(Frequency::Monthly {
            day: DayOfMonth::Weekday {
                weekday: Weekday::Tue,
            },
        });
        assert_eq!(
            repetition.between(d(2022, 1, 5), d(2022, 1, 1), d(2022, 4, 4)),
            vec!(d(2022, 2, 1), d(2022, 3, 1))
        )
    }

    #[test]
    fn yearly() {
        let repetition = from_freq(Frequency::Yearly);
        assert_eq!(
            repetition.between(d(2020, 5, 5), d(2018, 1, 1), d(2022, 5, 5)),
            vec!(d(2020, 5, 5), d(2021, 5, 5), d(2022, 5, 5))
        )
    }

    #[test]
    fn every_two_days_removed_occurence() {
        let repetition = Repetition {
            frequency: Frequency::Daily { period: 2 },
            removed_occurences: HashSet::from([0, 2, 3]),
            until: None,
        };
        assert_eq!(
            repetition.between(d(2020, 7, 1), d(2020, 7, 1), d(2020, 7, 9)),
            vec!(d(2020, 7, 3), d(2020, 7, 9))
        )
    }

    #[test]
    fn day_of_month_removed_occurence() {
        let repetition = Repetition {
            frequency: Frequency::Monthly {
                day: DayOfMonth::Day { day: 8 },
            },
            removed_occurences: HashSet::from([1, 3]),
            until: None,
        };
        assert_eq!(
            repetition.between(d(2020, 1, 8), d(2020, 1, 8), d(2020, 4, 8)),
            vec!(d(2020, 1, 8), d(2020, 3, 8))
        )
    }

    #[test]
    fn weekday_of_month_removed_occurence() {
        let repetition = Repetition {
            frequency: Frequency::Monthly {
                day: DayOfMonth::Weekday {
                    weekday: Weekday::Fri,
                },
            },
            removed_occurences: HashSet::from([1, 2, 3]),
            until: None,
        };
        assert_eq!(
            repetition.between(d(2020, 2, 1), d(2020, 2, 1), d(2020, 7, 1)),
            vec!(d(2020, 2, 7), d(2020, 6, 5))
        )
    }

    #[test]
    fn yearly_removed_occurence() {
        let repetition = Repetition {
            frequency: Frequency::Yearly,
            removed_occurences: HashSet::from([3]),
            until: None,
        };
        assert_eq!(
            repetition.between(d(2018, 5, 5), d(2019, 8, 1), d(2022, 5, 5)),
            vec!(d(2020, 5, 5), d(2022, 5, 5))
        )
    }

    #[test]
    fn occurence_index_after_removed_occurence() {
        let repetition = Repetition {
            frequency: Frequency::Yearly,
            removed_occurences: HashSet::from([1]),
            until: None,
        };
        assert_eq!(
            repetition.occurence_index(d(2020, 1, 1), d(2022, 1, 1)),
            Some(2)
        )
    }

    #[test]
    fn repetition_stops_after_until() {
        let repetition = Repetition {
            frequency: Frequency::Yearly,
            removed_occurences: HashSet::new(),
            until: Some(d(2022, 1, 1)),
        };
        assert_eq!(
            repetition.between(d(2020, 1, 1), d(2020, 1, 1), d(2024, 1, 1)),
            vec!(d(2020, 1, 1), d(2021, 1, 1), d(2022, 1, 1))
        )
    }

    #[test]
    fn repetition_daily_after_day() {
        let repetition = Repetition {
            frequency: Frequency::Daily { period: 1 },
            removed_occurences: HashSet::new(),
            until: Some(d(2022, 4, 17)),
        };
        assert_eq!(
            repetition.between(d(2022, 4, 4), d(2022, 3, 20), d(2022, 3, 20)),
            vec!()
        )
    }

    fn d(y: i32, m: u32, d: u32) -> NaiveDate {
        NaiveDate::from_ymd_opt(y, m, d)
    }

    fn from_freq(frequency: Frequency) -> Repetition {
        Repetition {
            frequency,
            removed_occurences: HashSet::new(),
            until: None,
        }
    }
}