/*
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::{DateTime, FixedOffset, NaiveDate};
use dimensioned::si;
use emseries::{Recordable, Timestamp};
use serde::{Deserialize, Serialize};

#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub enum SetRepActivity {
    Pushups,
    Situps,
}

pub const SET_REP_ACTIVITIES: [SetRepActivity; 2] =
    [SetRepActivity::Pushups, SetRepActivity::Situps];

/// SetRep represents workouts like pushups or situps, which involve doing a "set" of a number of
/// actions, resting, and then doing another set.
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct SetRep {
    /// I assume that a set/rep workout is only done once in a day.
    pub date: NaiveDate,
    /// The activity involved
    pub activity: SetRepActivity,
    /// Each set entry represents the number of times that the action was performed in a set. So, a
    /// pushup workout that involved five sets would have five entries. Each entry would be x
    /// number of pushups. A viable workout would be something like [6, 6, 4, 4, 5].
    pub sets: Vec<u32>,
    pub comments: Option<String>,
}

impl Recordable for SetRep {
    fn timestamp(&self) -> Timestamp {
        Timestamp::Date(self.date)
    }

    fn tags(&self) -> Vec<String> {
        vec![]
    }
}

/// The number of steps one takes in a single day.
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct Steps {
    pub date: NaiveDate,
    pub count: u32,
}

impl Recordable for Steps {
    fn timestamp(&self) -> Timestamp {
        Timestamp::Date(self.date)
    }

    fn tags(&self) -> Vec<String> {
        vec![]
    }
}

#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
pub enum TimeDistanceActivity {
    Biking,
    Running,
    Rowing,
    Swimming,
    Walking,
}

pub const TIME_DISTANCE_ACTIVITIES: [TimeDistanceActivity; 5] = [
    TimeDistanceActivity::Biking,
    TimeDistanceActivity::Rowing,
    TimeDistanceActivity::Running,
    TimeDistanceActivity::Swimming,
    TimeDistanceActivity::Walking,
];

/// TimeDistance represents workouts characterized by a duration and a distance travelled. These
/// sorts of workouts can occur many times a day, depending on how one records things. I might
/// record a single 30-km workout if I go on a long-distanec ride. Or I might record multiple 5km
/// workouts if I am out running errands. Distance and Duration are both optional because different
/// people have different priorities and may choose to measure different things.
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct TimeDistance {
    /// The precise time (and the relevant timezone) of the workout. One of the edge cases that I
    /// account for is that a ride which occurred at 11pm on one day in one timezone would then
    /// count as 1am on the next day if the user moves two timezones to the east. While technically
    /// correct, for most users this would throw off many years of metrics in ways that can be very
    /// hard to understand. Keeping the fixed offset means that we can have the most precise time
    /// in the database, but we can still get a Naive Date from the DateTime, which will still read
    /// as the original day.
    pub datetime: DateTime<FixedOffset>,
    /// The activity
    pub activity: TimeDistanceActivity,
    /// The distance travelled. This is optional because such a workout makes sense even without
    /// the distance.
    pub distance: Option<si::Meter<f64>>,
    /// The duration of the workout, which is also optional. Some people may keep track of the
    /// amount of distance travelled without tracking the duration.
    pub duration: Option<si::Second<f64>>,
    pub comments: Option<String>,
}

impl Recordable for TimeDistance {
    fn timestamp(&self) -> Timestamp {
        Timestamp::DateTime(self.datetime)
    }

    fn tags(&self) -> Vec<String> {
        vec![]
    }
}

/// A singular daily weight measurement. Weight changes slowly enough that it seems unlikely to
/// need to track more than a single weight in a day.
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct Weight {
    pub date: NaiveDate,
    pub weight: si::Kilogram<f64>,
}

impl Recordable for Weight {
    fn timestamp(&self) -> Timestamp {
        Timestamp::Date(self.date)
    }

    fn tags(&self) -> Vec<String> {
        vec![]
    }
}

#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub enum DurationWorkoutActivity {
    MartialArts,
    Yoga,
}

pub const DURATION_WORKOUT_ACTIVITIES: [DurationWorkoutActivity; 2] = [
    DurationWorkoutActivity::MartialArts,
    DurationWorkoutActivity::Yoga,
];

/// Generic workouts for which only duration really matters. This is for things
/// such as Martial Arts or Yoga, which are activities done for an amount of
/// time, but with no other details.
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct DurationWorkout {
    pub datetime: DateTime<FixedOffset>,
    pub activity: DurationWorkoutActivity,
    pub duration: Option<si::Second<f64>>,
    pub comments: Option<String>,
}

impl Recordable for DurationWorkout {
    fn timestamp(&self) -> Timestamp {
        Timestamp::DateTime(self.datetime)
    }

    fn tags(&self) -> Vec<String> {
        vec![]
    }
}

/// The unified data structure for all records that are part of the app.
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub enum TraxRecord {
    DurationWorkout(DurationWorkout),
    SetRep(SetRep),
    Steps(Steps),
    TimeDistance(TimeDistance),
    Weight(Weight),
}

impl TraxRecord {
    pub fn is_weight(&self) -> bool {
        matches!(self, TraxRecord::Weight(_))
    }

    pub fn is_steps(&self) -> bool {
        matches!(self, TraxRecord::Steps(_))
    }

    pub fn is_time_distance(&self) -> bool {
        matches!(
            self,
            TraxRecord::TimeDistance(TimeDistance {
                activity: TimeDistanceActivity::Biking,
                ..
            }) | TraxRecord::TimeDistance(TimeDistance {
                activity: TimeDistanceActivity::Running,
                ..
            }) | TraxRecord::TimeDistance(TimeDistance {
                activity: TimeDistanceActivity::Rowing,
                ..
            }) | TraxRecord::TimeDistance(TimeDistance {
                activity: TimeDistanceActivity::Swimming,
                ..
            }) | TraxRecord::TimeDistance(TimeDistance {
                activity: TimeDistanceActivity::Walking,
                ..
            })
        )
    }
}

impl Recordable for TraxRecord {
    fn timestamp(&self) -> Timestamp {
        match self {
            TraxRecord::TimeDistance(rec) => Timestamp::DateTime(rec.datetime),
            TraxRecord::SetRep(rec) => rec.timestamp(),
            TraxRecord::Steps(rec) => rec.timestamp(),
            TraxRecord::Weight(rec) => rec.timestamp(),
            TraxRecord::DurationWorkout(rec) => rec.timestamp(),
        }
    }

    fn tags(&self) -> Vec<String> {
        vec![]
    }
}

impl From<TimeDistance> for TraxRecord {
    fn from(td: TimeDistance) -> Self {
        Self::TimeDistance(td)
    }
}

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

    #[test]
    fn can_record_records() {
        let file = tempfile::NamedTempFile::new().expect("a temporary file");
        let path = file.into_temp_path();
        let mut series: Series<TraxRecord> = Series::open(&path).unwrap();

        let record = TraxRecord::Steps(Steps {
            date: NaiveDate::from_ymd_opt(2023, 1, 1).unwrap(),
            count: 1000,
        });

        let id = series.put(record.clone()).unwrap();

        let record_ = series.get(&id).unwrap();
        assert_eq!(record_.data, record);
    }
}
