/*
Copyright 2023-2024, Savanni D'Gerinel <savanni@luminescent-dreams.com>

This file is part of FitnessTrax.

FitnessTrax is free software: you can redistribute it and/or modify it under the terms of the GNU
General Public License as published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.

FitnessTrax is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
General Public License for more details.

You should have received a copy of the GNU General Public License along with FitnessTrax. If not, see <https://www.gnu.org/licenses/>.
*/

// use chrono::NaiveDate;
// use ft_core::TraxRecord;
use crate::{
    components::{
        steps_editor, time_distance_summary, weight_field, ActionGroup, Steps, WeightLabel,
    },
    types::{DistanceFormatter, DurationFormatter, WeightFormatter},
    view_models::DayDetailViewModel,
};
use emseries::{Record, RecordId};
use ft_core::{TimeDistanceActivity, TraxRecord, TIME_DISTANCE_ACTIVITIES};
use glib::Object;
use gtk::{prelude::*, subclass::prelude::*};
use std::{cell::RefCell, rc::Rc};

use super::{time_distance::TimeDistanceEdit, time_distance_detail};

pub struct DaySummaryPrivate {
    date: gtk::Label,
}

#[glib::object_subclass]
impl ObjectSubclass for DaySummaryPrivate {
    const NAME: &'static str = "DaySummary";
    type Type = DaySummary;
    type ParentType = gtk::Box;

    fn new() -> Self {
        let date = gtk::Label::builder()
            .css_classes(["day-summary__date"])
            .halign(gtk::Align::Start)
            .build();
        Self { date }
    }
}

impl ObjectImpl for DaySummaryPrivate {}
impl WidgetImpl for DaySummaryPrivate {}
impl BoxImpl for DaySummaryPrivate {}

glib::wrapper! {
    /// The DaySummary displays one day's activities in a narrative style. This is meant to give
    /// an overall feel of everything that happened during the day without going into details.
    pub struct DaySummary(ObjectSubclass<DaySummaryPrivate>) @extends gtk::Box, gtk::Widget, @implements gtk::Orientable;
}

impl Default for DaySummary {
    fn default() -> Self {
        let s: Self = Object::builder().build();
        s.set_orientation(gtk::Orientation::Vertical);
        s.set_css_classes(&["day-summary"]);

        s.append(&s.imp().date);

        s
    }
}

impl DaySummary {
    pub fn new() -> Self {
        Self::default()
    }

    pub fn set_data(&self, view_model: DayDetailViewModel) {
        self.imp()
            .date
            .set_text(&view_model.date.format("%Y-%m-%d").to_string());

        let row = gtk::Box::builder().build();

        let weight_label = gtk::Label::builder()
            .halign(gtk::Align::Start)
            .css_classes(["day-summary__weight"])
            .build();
        if let Some(w) = view_model.weight() {
            weight_label.set_label(&w.to_string())
        }

        let steps_label = gtk::Label::builder()
            .halign(gtk::Align::Start)
            .css_classes(["day-summary__steps"])
            .build();
        if let Some(s) = view_model.steps() {
            steps_label.set_label(&format!("{} steps", s));
        }

        row.append(&weight_label);
        row.append(&steps_label);
        self.append(&row);

        for activity in TIME_DISTANCE_ACTIVITIES {
            let summary = view_model.time_distance_summary(activity);
            if let Some(label) = time_distance_summary(
                activity,
                DistanceFormatter::from(summary.0),
                DurationFormatter::from(summary.1),
            ) {
                self.append(&label);
            }
        }
    }
}

#[derive(Default)]
pub struct DayDetailPrivate {}

#[glib::object_subclass]
impl ObjectSubclass for DayDetailPrivate {
    const NAME: &'static str = "DayDetail";
    type Type = DayDetail;
    type ParentType = gtk::Box;
}

impl ObjectImpl for DayDetailPrivate {}
impl WidgetImpl for DayDetailPrivate {}
impl BoxImpl for DayDetailPrivate {}

glib::wrapper! {
    pub struct DayDetail(ObjectSubclass<DayDetailPrivate>) @extends gtk::Box, gtk::Widget, @implements gtk::Orientable;
}

impl DayDetail {
    pub fn new<OnEdit>(view_model: DayDetailViewModel, on_edit: OnEdit) -> Self
    where
        OnEdit: Fn() + 'static,
    {
        let s: Self = Object::builder().build();
        s.set_orientation(gtk::Orientation::Vertical);
        s.set_hexpand(true);

        s.append(
            &ActionGroup::builder()
                .primary_action("Edit", Box::new(on_edit))
                .build(),
        );

        let top_row = gtk::Box::builder()
            .orientation(gtk::Orientation::Horizontal)
            .build();
        let weight_view = WeightLabel::new(view_model.weight().map(WeightFormatter::from));
        top_row.append(&weight_view.widget());

        let steps_view = Steps::new(view_model.steps());
        top_row.append(&steps_view.widget());

        s.append(&top_row);

        let records = view_model.time_distance_records();
        for emseries::Record { data, .. } in records {
            s.append(&time_distance_detail(data));
        }

        s
    }
}

pub struct DayEditPrivate {
    on_finished: RefCell<Box<dyn Fn()>>,
    #[allow(unused)]
    workout_rows: RefCell<gtk::Box>,
    view_model: RefCell<Option<DayDetailViewModel>>,
}

impl Default for DayEditPrivate {
    fn default() -> Self {
        Self {
            on_finished: RefCell::new(Box::new(|| {})),
            workout_rows: RefCell::new(
                gtk::Box::builder()
                    .orientation(gtk::Orientation::Vertical)
                    .hexpand(true)
                    .build(),
            ),
            view_model: RefCell::new(None),
        }
    }
}

#[glib::object_subclass]
impl ObjectSubclass for DayEditPrivate {
    const NAME: &'static str = "DayEdit";
    type Type = DayEdit;
    type ParentType = gtk::Box;
}

impl ObjectImpl for DayEditPrivate {}
impl WidgetImpl for DayEditPrivate {}
impl BoxImpl for DayEditPrivate {}

glib::wrapper! {
    pub struct DayEdit(ObjectSubclass<DayEditPrivate>) @extends gtk::Box, gtk::Widget, @implements gtk::Orientable;
}

impl DayEdit {
    pub fn new<OnFinished>(view_model: DayDetailViewModel, on_finished: OnFinished) -> Self
    where
        OnFinished: Fn() + 'static,
    {
        let s: Self = Object::builder().build();
        s.set_orientation(gtk::Orientation::Vertical);
        s.set_hexpand(true);
        *s.imp().on_finished.borrow_mut() = Box::new(on_finished);
        *s.imp().view_model.borrow_mut() = Some(view_model.clone());

        let workout_buttons = workout_buttons(view_model.clone(), {
            let s = s.clone();
            move |workout| s.add_row(workout)
        });

        view_model
            .records()
            .into_iter()
            .filter_map({
                let s = s.clone();
                move |record| match record.data {
                    TraxRecord::TimeDistance(workout) => Some(TimeDistanceEdit::new(workout, {
                        let s = s.clone();
                        move |data| {
                            s.update_workout(record.id, data);
                        }
                    })),
                    _ => None,
                }
            })
            .for_each(|row| s.imp().workout_rows.borrow().append(&row));

        s.append(&control_buttons(&s, &view_model));
        s.append(&weight_and_steps_row(&view_model));
        s.append(&*s.imp().workout_rows.borrow());
        s.append(&workout_buttons);

        s
    }

    fn finish(&self) {
        glib::spawn_future_local({
            let s = self.clone();
            async move {
                let view_model = {
                    let view_model = s.imp().view_model.borrow();
                    view_model
                        .as_ref()
                        .expect("DayEdit has not been initialized with the view model")
                        .clone()
                };
                let _ = view_model.async_save().await;
                (s.imp().on_finished.borrow())()
            }
        });
    }

    fn add_row(&self, workout: Record<TraxRecord>) {
        let workout_rows = self.imp().workout_rows.borrow();

        #[allow(clippy::single_match)]
        match workout.data {
            TraxRecord::TimeDistance(r) => workout_rows.append(&TimeDistanceEdit::new(r, {
                let s = self.clone();
                move |data| {
                    println!("update workout callback on workout: {:?}", workout.id);
                    s.update_workout(workout.id, data)
                }
            })),

            _ => {}
        }
    }

    fn update_workout(&self, id: RecordId, data: ft_core::TimeDistance) {
        if let Some(ref view_model) = *self.imp().view_model.borrow() {
            let record = Record {
                id,
                data: TraxRecord::TimeDistance(data),
            };
            view_model.update_record(record);
        }
    }
}

fn control_buttons(s: &DayEdit, view_model: &DayDetailViewModel) -> ActionGroup {
    ActionGroup::builder()
        .primary_action("Save", {
            let s = s.clone();
            move || s.finish()
        })
        .secondary_action("Cancel", {
            let s = s.clone();
            let view_model = view_model.clone();
            move || {
                let s = s.clone();
                let view_model = view_model.clone();
                glib::spawn_future_local(async move {
                    view_model.revert().await;
                    s.finish();
                });
            }
        })
        .build()
}

fn weight_and_steps_row(view_model: &DayDetailViewModel) -> gtk::Box {
    let row = gtk::Box::builder()
        .orientation(gtk::Orientation::Horizontal)
        .build();
    row.append(
        &weight_field(view_model.weight().map(WeightFormatter::from), {
            let view_model = view_model.clone();
            move |w| match w {
                Some(w) => view_model.set_weight(*w),
                None => eprintln!("have not implemented record delete"),
            }
        })
        .widget(),
    );

    row.append(
        &steps_editor(view_model.steps(), {
            let view_model = view_model.clone();
            move |s| match s {
                Some(s) => view_model.set_steps(s),
                None => eprintln!("have not implemented record delete"),
            }
        })
        .widget(),
    );

    row
}

fn workout_buttons<AddRow>(view_model: DayDetailViewModel, add_row: AddRow) -> gtk::Box
where
    AddRow: Fn(Record<TraxRecord>) + 'static,
{
    let add_row = Rc::new(add_row);

    let layout = gtk::Box::builder()
        .orientation(gtk::Orientation::Horizontal)
        .build();

    for (activity, icon, label) in [
        (
            TimeDistanceActivity::Biking,
            "cycling-symbolic",
            "Bike Ride",
        ),
        (TimeDistanceActivity::Rowing, "rowing-symbolic", "Rowing"),
        (TimeDistanceActivity::Running, "running-symbolic", "Run"),
        (TimeDistanceActivity::Swimming, "swimming-symbolic", "Swim"),
        (TimeDistanceActivity::Walking, "walking-symbolic", "Walk"),
    ] {
        let button = workout_button(activity, icon, label, view_model.clone(), {
            let add_row = add_row.clone();
            move |record| add_row(record)
        });
        layout.append(&button);
    }

    layout
}

fn workout_button<AddRow>(
    activity: TimeDistanceActivity,
    _icon: &str,
    label: &str,
    view_model: DayDetailViewModel,
    add_row: AddRow,
) -> gtk::Button
where
    AddRow: Fn(Record<TraxRecord>) + 'static,
{
    let button = gtk::Button::builder()
        .label(label)
        .width_request(64)
        .height_request(64)
        .build();
    button.connect_clicked({
        let view_model = view_model.clone();
        move |_| {
            let workout = view_model.new_time_distance(activity);
            add_row(workout.map(TraxRecord::TimeDistance));
        }
    });
    button
}
