use chrono::Weekday::Sun;
use chrono::{Datelike, Days, Duration, LocalResult, NaiveDate, NaiveTime, TimeZone};
use chronoutil::DateRule;
use color_eyre::eyre::{bail, eyre, Result, WrapErr};
use itertools::Itertools;
use num_traits::cast::FromPrimitive;
use std::fs::create_dir_all;
use std::path::Path;
use std::{collections::BTreeMap, iter, path::PathBuf};
use super::week_view::WeekMap;
use crate::configuration::types::calendar_view::CalendarView;
use crate::model::month::Month;
use crate::{
configuration::config::Config,
model::{
calendar_collection::{CalendarCollection, LocalDay},
day::DayContext,
},
views::week_view::WeekDayMap,
};
type InternalDate = NaiveDate;
pub type MonthMap<'a> = BTreeMap<Month<'a>, WeekMap<'a>>;
pub type MonthSlice<'a> = &'a [Option<Month<'a>>];
pub(crate) const VIEW_PATH: &str = "month";
const PAGE_TITLE: &str = "Month Page";
#[derive(Debug)]
pub struct MonthView<'a> {
calendars: &'a CalendarCollection,
output_dir: PathBuf,
}
impl MonthView<'_> {
pub fn new(calendars: &CalendarCollection) -> MonthView<'_> {
let output_dir = calendars
.base_dir()
.join(&calendars.config.output_dir)
.join(VIEW_PATH);
MonthView {
calendars,
output_dir,
}
}
fn config(&self) -> &Config {
&self.calendars.config
}
fn output_dir(&self) -> &Path {
&self.output_dir
}
fn months_to_show(&self) -> Result<Vec<Option<Month>>, color_eyre::eyre::Error> {
let aligned_month_start = self
.calendars
.cal_start
.with_day(1)
.ok_or(eyre!("could not get aligned start of month"))?;
let aligned_month_end = DateRule::monthly(self.calendars.cal_end)
.with_rolling_day(31)
.map_err(|e| eyre!(e))
.wrap_err("could not create an iterator with rolling day at end of month")?
.next()
.ok_or(eyre!("could not get end of month"))?;
let months_to_show = DateRule::monthly(aligned_month_start)
.with_end(aligned_month_end)
.with_rolling_day(1)
.map_err(|e| eyre!(e))
.wrap_err("could not create month iterator")?;
let chained_iter = iter::once(None)
.chain(
months_to_show
.into_iter()
.map(|m| Some(Month::new(self.calendars, m.date_naive()))),
)
.chain(iter::once(None));
let month_windows = chained_iter.collect::<Vec<Option<Month>>>();
Ok(month_windows)
}
pub fn create_html_pages(&self) -> Result<()> {
create_dir_all(self.output_dir())?;
let mut index_written = false;
for window in self.months_to_show()?.windows(3) {
let next_month_opt = window[2];
let mut index_paths = vec![];
if !index_written {
if let Some(next_month) = next_month_opt {
if next_month
> self.calendars.today_date().with_day(1).ok_or(eyre!(
"could not convert agenda start date to beginning of month"
))?
{
index_written = true;
index_paths.push(self.output_dir().join(PathBuf::from("index.html")));
if self.config().default_calendar_view == CalendarView::Month {
index_paths
.push(self.config().output_dir.join(PathBuf::from("index.html")));
}
}
} else {
index_written = true;
index_paths.push(self.output_dir().join(PathBuf::from("index.html")));
if self.config().default_calendar_view == CalendarView::Month {
index_paths
.push(self.config().output_dir.join(PathBuf::from("index.html")));
}
}
}
self.write_view(&window, index_paths.as_slice())?;
}
Ok(())
}
fn write_view(&self, month_slice: &MonthSlice, index_paths: &[PathBuf]) -> Result<()> {
let previous_month = month_slice[0];
let current_month =
month_slice[1].expect("Current month is None. This should never happen.");
let next_month = month_slice[2];
println!("month: {:?}", current_month);
let mut week_list = Vec::new();
let days_by_week = month_view_date_range(current_month)?.chunks(7);
let weeks_for_display = days_by_week.into_iter();
for (week_num, week) in weeks_for_display.enumerate() {
println!("From week {}:", week_num);
let mut week_dates = Vec::new();
for day in week {
let events = self
.calendars
.events_by_day
.get(
&day.with_timezone::<chrono_tz::Tz>(&self.config().display_timezone.into())
.date_naive(),
);
println!(
" For week {} day {}: there are {} events",
week_num,
day,
events.map(|e| e.len()).unwrap_or(0)
);
week_dates.push(DayContext::new(
day.naive_local().date(),
events
.map(|l| {
l.iter()
.sorted()
.map(|e| e.context(&self.calendars.config))
.collect()
})
.unwrap_or_default(),
));
}
week_list.push(week_dates);
}
let file_name = format!("{}-{}.html", current_month.year(), current_month.month());
let previous_file_name = previous_month.map(|previous_month| {
format!("{}-{}.html", previous_month.year(), previous_month.month())
});
let next_file_name = next_month
.map(|next_month| format!("{}-{}.html", next_month.year(), next_month.month()));
let mut context = self.calendars.template_context();
context.insert("week_view_path", ¤t_month.week_view_path());
context.insert("day_view_path", ¤t_month.day_view_path());
if let Some(first_event) = ¤t_month
.first_event()
.wrap_err("could not get first event")?
{
context.insert("event_view_path", &first_event.file_path());
}
context.insert("current_view", VIEW_PATH);
context.insert("page_title", PAGE_TITLE);
context.insert(
"view_date",
¤t_month
.naive_date()
.ok_or(eyre!("could not get naive date"))?
.format(&self.config().month_view_format)
.to_string(),
);
context.insert("year", ¤t_month.year());
context.insert("month", ¤t_month.month());
context.insert(
"month_name",
&chrono::Month::from_u8(current_month.month() as u8)
.ok_or(eyre!("unknown month"))?
.name(),
);
context.insert("weeks", &week_list);
let binding = self.output_dir().join(PathBuf::from(&file_name));
let mut file_paths = vec![&binding];
file_paths.extend(index_paths);
let base_url_path: unix_path::PathBuf =
self.calendars.config.base_url_path.path_buf().clone();
for file_path in file_paths {
let view_path = base_url_path.join("month");
context.insert(
"previous_file_name",
&previous_file_name.as_ref().map(|path| view_path.join(path)),
);
context.insert(
"next_file_name",
&next_file_name.as_ref().map(|path| view_path.join(path)),
);
self.calendars
.write_template("month.html", &context, file_path)?;
}
Ok(())
}
}
fn month_view_date_range(month: Month) -> Result<DateRule<LocalDay>> {
let first_day_of_month = month
.naive_date()
.ok_or(eyre!("could not retrieve first day of month"))?
.with_day(1)
.ok_or(eyre!("could not get first day of month"))?;
let last_day_of_month = DateRule::monthly(first_day_of_month)
.with_rolling_day(31)
.map_err(|e| eyre!(e))
.wrap_err("could not create rolling day rule")?
.next()
.ok_or(eyre!("could not get last day of month"))?;
let first_day_of_view =
first_day_of_month - Days::new(first_day_of_month.weekday().num_days_from_sunday().into());
let last_day_of_view = last_day_of_month
+ Days::new(((7 - last_day_of_month.weekday().num_days_from_sunday()) % 7).into());
let start_datetime = match month
.timezone()
.from_local_datetime(&first_day_of_view.and_time(NaiveTime::MIN))
{
LocalResult::None => bail!("could not get start_datetime"),
LocalResult::Single(t) => t,
LocalResult::Ambiguous(t, _) => t,
};
let end_datetime = match month
.timezone()
.from_local_datetime(&last_day_of_view.and_time(NaiveTime::MIN))
{
LocalResult::None => bail!("could not get end_datetime"),
LocalResult::Single(t) => t,
LocalResult::Ambiguous(t, _) => t,
};
Ok(DateRule::daily(start_datetime).with_end(end_datetime))
}
fn first_sunday_of_week(year: &i32, week: &u32) -> Result<InternalDate, color_eyre::Report> {
let first_sunday_of_month =
NaiveDate::from_isoywd_opt(*year, *week, Sun).ok_or(eyre!("could not get iso week"))?;
Ok(first_sunday_of_month)
}
pub trait WeekContext {
fn context(&self, year: &i32, week: &u8, config: &Config) -> Result<Vec<DayContext>>;
}
impl WeekContext for WeekDayMap {
fn context(&self, year: &i32, week: &u8, config: &Config) -> Result<Vec<DayContext>> {
let sunday = first_sunday_of_week(year, &(*week as u32))?;
let week_dates: Vec<DayContext> = [0_u8, 1_u8, 2_u8, 3_u8, 4_u8, 5_u8, 6_u8]
.iter()
.map(|o| {
DayContext::new(
sunday + Duration::days(*o as i64),
self.get(o)
.map(|l| l.iter().map(|e| e.context(config)).collect())
.unwrap_or_default(),
)
})
.collect();
Ok(week_dates)
}
}