// Copyright 2024 System76 <info@system76.com>
// SPDX-License-Identifier: MPL-2.0

//! A widget that displays an interactive calendar.

use crate::fl;
use crate::widget::{button, column, grid, icon, row, text};
use apply::Apply;
use iced::alignment::Vertical;
use iced_core::{Alignment, Length};
use jiff::ToSpan;
use jiff::civil::{Date, Weekday};

/// A widget that displays an interactive calendar.
pub fn calendar<M>(
    model: &CalendarModel,
    on_select: impl Fn(Date) -> M + 'static,
    on_prev: impl Fn() -> M + 'static,
    on_next: impl Fn() -> M + 'static,
    first_day_of_week: Weekday,
) -> Calendar<'_, M> {
    Calendar {
        model,
        on_select: Box::new(on_select),
        on_prev: Box::new(on_prev),
        on_next: Box::new(on_next),
        first_day_of_week,
    }
}

pub fn set_day(date_selected: Date, day: i8) -> Date {
    date_selected
        .with()
        .day(day)
        .build()
        .unwrap_or(date_selected)
}

#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug, Hash)]
pub struct CalendarModel {
    pub selected: Date,
    pub visible: Date,
}

impl CalendarModel {
    pub fn now() -> Self {
        let now = jiff::Zoned::now().date();
        CalendarModel {
            selected: now,
            visible: now,
        }
    }

    #[inline]
    pub fn new(selected: Date, visible: Date) -> Self {
        CalendarModel { selected, visible }
    }

    pub fn show_prev_month(&mut self) {
        self.visible = self.visible.checked_sub(1.month()).expect("valid date");
    }

    pub fn show_next_month(&mut self) {
        self.visible = self.visible.checked_add(1.month()).expect("valid date");
    }

    #[inline]
    pub fn set_prev_month(&mut self) {
        self.show_prev_month();
        self.selected = self.visible;
    }

    #[inline]
    pub fn set_next_month(&mut self) {
        self.show_next_month();
        self.selected = self.visible;
    }

    #[inline]
    pub fn set_selected_visible(&mut self, selected: Date) {
        self.selected = selected;
        self.visible = self.selected;
    }
}

pub struct Calendar<'a, M> {
    model: &'a CalendarModel,
    on_select: Box<dyn Fn(Date) -> M>,
    on_prev: Box<dyn Fn() -> M>,
    on_next: Box<dyn Fn() -> M>,
    first_day_of_week: Weekday,
}

impl<'a, Message> From<Calendar<'a, Message>> for crate::Element<'a, Message>
where
    Message: Clone + 'static,
{
    fn from(this: Calendar<'a, Message>) -> Self {
        macro_rules! translate_month {
            ($month:expr, $year:expr) => {{
                match $month {
                    1 => fl!("january", year = $year),
                    2 => fl!("february", year = $year),
                    3 => fl!("march", year = $year),
                    4 => fl!("april", year = $year),
                    5 => fl!("may", year = $year),
                    6 => fl!("june", year = $year),
                    7 => fl!("july", year = $year),
                    8 => fl!("august", year = $year),
                    9 => fl!("september", year = $year),
                    10 => fl!("october", year = $year),
                    11 => fl!("november", year = $year),
                    12 => fl!("december", year = $year),
                    _ => unreachable!(),
                }
            }};
        }
        macro_rules! translate_weekday {
            ($weekday:expr, short) => {{
                match $weekday {
                    Weekday::Monday => fl!("mon"),
                    Weekday::Tuesday => fl!("tue"),
                    Weekday::Wednesday => fl!("wed"),
                    Weekday::Thursday => fl!("thu"),
                    Weekday::Friday => fl!("fri"),
                    Weekday::Saturday => fl!("sat"),
                    Weekday::Sunday => fl!("sun"),
                }
            }};
            ($weekday:expr, long) => {{
                match $weekday {
                    Weekday::Monday => fl!("monday"),
                    Weekday::Tuesday => fl!("tuesday"),
                    Weekday::Wednesday => fl!("wednesday"),
                    Weekday::Thursday => fl!("thursday"),
                    Weekday::Friday => fl!("friday"),
                    Weekday::Saturday => fl!("saturday"),
                    Weekday::Sunday => fl!("sunday"),
                }
            }};
        }

        let date = text(translate_month!(
            this.model.visible.month(),
            this.model.visible.year()
        ))
        .size(18);

        let day = text::body(translate_weekday!(this.model.visible.weekday(), long));

        let month_controls = row::with_capacity(2)
            .spacing(8)
            .push(
                icon::from_name("go-previous-symbolic")
                    .apply(button::icon)
                    .on_press((this.on_prev)()),
            )
            .push(
                icon::from_name("go-next-symbolic")
                    .apply(button::icon)
                    .on_press((this.on_next)()),
            );

        // Calendar
        let mut calendar_grid = grid().padding([0, 12].into()).width(Length::Fill);

        let mut first_day_of_week = this.first_day_of_week;
        for _ in 0..7 {
            calendar_grid = calendar_grid.push(
                text::caption(translate_weekday!(first_day_of_week, short))
                    .width(Length::Fixed(44.0))
                    .align_x(Alignment::Center),
            );

            first_day_of_week = first_day_of_week.next();
        }
        calendar_grid = calendar_grid.insert_row();

        let first = get_calendar_first(
            this.model.visible.year(),
            this.model.visible.month(),
            this.first_day_of_week,
        );

        let today = jiff::Zoned::now().date();
        for i in 0..42 {
            if i > 0 && i % 7 == 0 {
                calendar_grid = calendar_grid.insert_row();
            }

            let date = first
                .checked_add(i.days())
                .expect("valid date in calendar range");
            let is_currently_viewed_month =
                date.first_of_month() == this.model.visible.first_of_month();
            let is_currently_selected_month =
                date.first_of_month() == this.model.selected.first_of_month();
            let is_currently_selected_day =
                date.day() == this.model.selected.day() && is_currently_selected_month;
            let is_today = date == today;

            calendar_grid = calendar_grid.push(date_button(
                date,
                is_currently_viewed_month,
                is_currently_selected_day,
                is_today,
                &this.on_select,
            ));
        }

        let content_list = column::with_children([
            row::with_children([
                column([date.into(), day.into()]).into(),
                crate::widget::space::horizontal()
                    .width(Length::Fill)
                    .into(),
                month_controls.into(),
            ])
            .align_y(Vertical::Center)
            .padding([12, 20])
            .into(),
            calendar_grid.into(),
        ])
        .width(360)
        .padding([8, 0]);

        Self::new(content_list)
    }
}

fn date_button<Message: Clone + 'static>(
    date: Date,
    is_currently_viewed_month: bool,
    is_currently_selected_day: bool,
    is_today: bool,
    on_select: &dyn Fn(Date) -> Message,
) -> crate::widget::Button<'static, Message> {
    let style = if is_currently_selected_day {
        button::ButtonClass::Suggested
    } else if is_today {
        button::ButtonClass::Standard
    } else {
        button::ButtonClass::Text
    };

    let button = button::custom(text(format!("{}", date.day())).center())
        .class(style)
        .height(Length::Fixed(44.0))
        .width(Length::Fixed(44.0));

    if is_currently_viewed_month {
        button.on_press((on_select)(set_day(date, date.day())))
    } else {
        button
    }
}

/// Gets the first date that will be visible on the calendar
#[must_use]
pub fn get_calendar_first(year: i16, month: i8, from_weekday: Weekday) -> Date {
    let date = Date::new(year, month, 1).expect("valid date");
    let num_days = date.weekday().since(from_weekday);
    date.checked_sub(num_days.days()).expect("valid date")
}
