use chrono::prelude::*;
use lazy_static::lazy_static;
use serde_derive::{Deserialize, Serialize};
use std::collections::HashMap;

// http://astropixels.com/ephemeris/soleq2001.html
const SOLSTICE_TEXT: &str = "
 2001      Mar 20  13:31     Jun 21  07:38     Sep 22  23:05     Dec 21  19:22
 2002      Mar 20  19:16     Jun 21  13:25     Sep 23  04:56     Dec 22  01:15
 2003      Mar 21  01:00     Jun 21  19:11     Sep 23  10:47     Dec 22  07:04
 2004      Mar 20  06:49     Jun 21  00:57     Sep 22  16:30     Dec 21  12:42
 2005      Mar 20  12:34     Jun 21  06:46     Sep 22  22:23     Dec 21  18:35
 2006      Mar 20  18:25     Jun 21  12:26     Sep 23  04:04     Dec 22  00:22
 2007      Mar 21  00:07     Jun 21  18:06     Sep 23  09:51     Dec 22  06:08
 2008      Mar 20  05:49     Jun 21  00:00     Sep 22  15:45     Dec 21  12:04
 2009      Mar 20  11:44     Jun 21  05:45     Sep 22  21:18     Dec 21  17:47
 2010      Mar 20  17:32     Jun 21  11:28     Sep 23  03:09     Dec 21  23:38

 2011      Mar 20  23:21     Jun 21  17:16     Sep 23  09:05     Dec 22  05:30
 2012      Mar 20  05:15     Jun 20  23:08     Sep 22  14:49     Dec 21  11:12
 2013      Mar 20  11:02     Jun 21  05:04     Sep 22  20:44     Dec 21  17:11
 2014      Mar 20  16:57     Jun 21  10:52     Sep 23  02:30     Dec 21  23:03
 2015      Mar 20  22:45     Jun 21  16:38     Sep 23  08:20     Dec 22  04:48
 2016      Mar 20  04:31     Jun 20  22:35     Sep 22  14:21     Dec 21  10:45
 2017      Mar 20  10:29     Jun 21  04:25     Sep 22  20:02     Dec 21  16:29
 2018      Mar 20  16:15     Jun 21  10:07     Sep 23  01:54     Dec 21  22:22
 2019      Mar 20  21:58     Jun 21  15:54     Sep 23  07:50     Dec 22  04:19
 2020      Mar 20  03:50     Jun 20  21:43     Sep 22  13:31     Dec 21  10:03

 2021      Mar 20  09:37     Jun 21  03:32     Sep 22  19:21     Dec 21  15:59
 2022      Mar 20  15:33     Jun 21  09:14     Sep 23  01:04     Dec 21  21:48
 2023      Mar 20  21:25     Jun 21  14:58     Sep 23  06:50     Dec 22  03:28
 2024      Mar 20  03:07     Jun 20  20:51     Sep 22  12:44     Dec 21  09:20
 2025      Mar 20  09:02     Jun 21  02:42     Sep 22  18:20     Dec 21  15:03
 2026      Mar 20  14:46     Jun 21  08:25     Sep 23  00:06     Dec 21  20:50
 2027      Mar 20  20:25     Jun 21  14:11     Sep 23  06:02     Dec 22  02:43
 2028      Mar 20  02:17     Jun 20  20:02     Sep 22  11:45     Dec 21  08:20
 2029      Mar 20  08:01     Jun 21  01:48     Sep 22  17:37     Dec 21  14:14
 2030      Mar 20  13:51     Jun 21  07:31     Sep 22  23:27     Dec 21  20:09

 2031      Mar 20  19:41     Jun 21  13:17     Sep 23  05:15     Dec 22  01:56
 2032      Mar 20  01:23     Jun 20  19:09     Sep 22  11:11     Dec 21  07:57
 2033      Mar 20  07:23     Jun 21  01:01     Sep 22  16:52     Dec 21  13:45
 2034      Mar 20  13:18     Jun 21  06:45     Sep 22  22:41     Dec 21  19:35
 2035      Mar 20  19:03     Jun 21  12:33     Sep 23  04:39     Dec 22  01:31
 2036      Mar 20  01:02     Jun 20  18:31     Sep 22  10:23     Dec 21  07:12
 2037      Mar 20  06:50     Jun 21  00:22     Sep 22  16:13     Dec 21  13:08
 2038      Mar 20  12:40     Jun 21  06:09     Sep 22  22:02     Dec 21  19:01
 2039      Mar 20  18:32     Jun 21  11:58     Sep 23  03:50     Dec 22  00:41
 2040      Mar 20  00:11     Jun 20  17:46     Sep 22  09:44     Dec 21  06:33

 2041      Mar 20  06:07     Jun 20  23:37     Sep 22  15:27     Dec 21  12:19
 2042      Mar 20  11:53     Jun 21  05:16     Sep 22  21:11     Dec 21  18:04
 2043      Mar 20  17:29     Jun 21  10:59     Sep 23  03:07     Dec 22  00:02
 2044      Mar 19  23:20     Jun 20  16:50     Sep 22  08:47     Dec 21  05:43
 2045      Mar 20  05:08     Jun 20  22:34     Sep 22  14:33     Dec 21  11:36
 2046      Mar 20  10:58     Jun 21  04:15     Sep 22  20:22     Dec 21  17:28
 2047      Mar 20  16:52     Jun 21  10:02     Sep 23  02:07     Dec 21  23:07
 2048      Mar 19  22:34     Jun 20  15:54     Sep 22  08:01     Dec 21  05:02
 2049      Mar 20  04:28     Jun 20  21:47     Sep 22  13:42     Dec 21  10:51
 2050      Mar 20  10:20     Jun 21  03:33     Sep 22  19:29     Dec 21  16:39
";

#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
pub struct YearlyEvents {
    pub year: i32,
    pub spring_equinox: chrono::DateTime<chrono::Utc>,
    pub summer_solstice: chrono::DateTime<chrono::Utc>,
    pub autumn_equinox: chrono::DateTime<chrono::Utc>,
    pub winter_solstice: chrono::DateTime<chrono::Utc>,
}

#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
pub enum Event {
    SpringEquinox(chrono::DateTime<chrono::Utc>),
    SummerSolstice(chrono::DateTime<chrono::Utc>),
    AutumnEquinox(chrono::DateTime<chrono::Utc>),
    WinterSolstice(chrono::DateTime<chrono::Utc>),
}

impl Event {
    pub fn date(&self) -> chrono::DateTime<chrono::Utc> {
        match *self {
            Event::SpringEquinox(d) => d,
            Event::SummerSolstice(d) => d,
            Event::AutumnEquinox(d) => d,
            Event::WinterSolstice(d) => d,
        }
    }
}

fn parse_time<'a>(
    year: &str,
    iter: impl Iterator<Item = &'a str>,
) -> chrono::DateTime<chrono::Utc> {
    let parts = iter.collect::<Vec<&str>>();
    let p = format!("{} {} {} {}", year, parts[0], parts[1], parts[2]);
    NaiveDateTime::parse_from_str(&p, "%Y %b %d %H:%M")
        .unwrap()
        .and_utc()
}

fn parse_line(year: &str, rest: &[&str]) -> YearlyEvents {
    let spring = parse_time(year, rest.iter().take(3).cloned());
    let summer = parse_time(year, rest.iter().skip(3).take(3).cloned());
    let autumn = parse_time(year, rest.iter().skip(6).take(3).cloned());
    let winter = parse_time(year, rest.iter().skip(9).take(3).cloned());
    YearlyEvents {
        year: year.parse::<i32>().unwrap(),
        spring_equinox: spring,
        summer_solstice: summer,
        autumn_equinox: autumn,
        winter_solstice: winter,
    }
}

fn parse_events() -> Vec<Option<YearlyEvents>> {
    SOLSTICE_TEXT
        .lines()
        .map(|line| {
            match line
                .split(' ')
                .filter(|elem| !elem.is_empty())
                .collect::<Vec<&str>>()
                .as_slice()
            {
                [year, rest @ ..] => Some(parse_line(year, rest)),
                _ => None,
            }
        })
        .collect()
}

pub struct Solstices(HashMap<i32, YearlyEvents>);

impl Solstices {
    pub fn yearly_events(&self, year: i32) -> Option<YearlyEvents> {
        self.0.get(&year).copied()
    }

    pub fn next_event(&self, date: chrono::DateTime<chrono::Utc>) -> Option<Event> {
        let year_events = self.0.get(&date.year());
        match year_events {
            Some(year_events) => {
                if date <= year_events.spring_equinox {
                    Some(Event::SpringEquinox(year_events.spring_equinox))
                } else if date <= year_events.summer_solstice {
                    Some(Event::SummerSolstice(year_events.summer_solstice))
                } else if date <= year_events.autumn_equinox {
                    Some(Event::AutumnEquinox(year_events.autumn_equinox))
                } else if date <= year_events.winter_solstice {
                    Some(Event::WinterSolstice(year_events.winter_solstice))
                } else {
                    self.0
                        .get(&(date.year() + 1))
                        .map(|_| Event::SpringEquinox(year_events.spring_equinox))
                }
            }
            None => None,
        }
    }
}

impl From<Vec<Option<YearlyEvents>>> for Solstices {
    fn from(event_list: Vec<Option<YearlyEvents>>) -> Self {
        Solstices(event_list.iter().fold(HashMap::new(), |mut m, record| {
            match record {
                Some(record) => {
                    m.insert(record.year, *record);
                }
                None => (),
            }
            m
        }))
    }
}

lazy_static! {
    pub static ref EVENTS: Solstices = Solstices::from(parse_events());
}

#[cfg(test)]
mod test {
    use chrono::{NaiveDate, NaiveDateTime};

    #[test]
    fn it_can_parse_a_solstice_time() {
        let p = "2001 Mar 20 13:31".to_owned();
        let parsed_date = NaiveDateTime::parse_from_str(&p, "%Y %b %d %H:%M")
            .unwrap()
            .and_utc();
        assert_eq!(
            parsed_date,
            NaiveDate::from_ymd_opt(2001, 03, 20)
                .unwrap()
                .and_hms_opt(13, 31, 0)
                .unwrap()
                .and_utc()
        );
    }
}
