/*
Copyright 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::SecondsFormat;
use chrono_tz::Etc::UTC;
use dimensioned::si;
use emseries::{Record, RecordId, Series, Timestamp};
use ft_core::{self, DurationWorkout, DurationWorkoutActivity, SetRepActivity, TraxRecord};
use serde::{
    de::{self, Visitor},
    Deserialize, Deserializer, Serialize, Serializer,
};
use std::{
    fmt,
    fs::File,
    io::{BufRead, BufReader, Read},
    str::FromStr,
};

/// This is a wrapper around date time objects, using timezones from the chroon-tz database and
/// providing string representation and parsing of the form "<RFC3339> <Timezone Name>", i.e.,
/// "2019-05-15T14:30:00Z US/Central". The to_string method, and serde serialization will
/// produce a string of this format. The parser will accept an RFC3339-only string of the forms
/// "2019-05-15T14:30:00Z", "2019-05-15T14:30:00+00:00", and also an "RFC3339 Timezone Name"
/// string.
///
/// The function here is to generate as close to unambiguous time/date strings, (for earth's
/// gravitational frame of reference), as possible. Clumping together the time, offset from UTC,
/// and the named time zone allows future parsers to know the exact interpretation of the time in
/// the frame of reference of the original recording.
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub struct DateTimeTz(pub chrono::DateTime<chrono_tz::Tz>);

impl fmt::Display for DateTimeTz {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
        if self.0.timezone() == UTC {
            write!(f, "{}", self.0.to_rfc3339_opts(SecondsFormat::Secs, true))
        } else {
            write!(
                f,
                "{} {}",
                self.0
                    .with_timezone(&chrono_tz::Etc::UTC)
                    .to_rfc3339_opts(SecondsFormat::Secs, true,),
                self.0.timezone().name()
            )
        }
    }
}

impl DateTimeTz {
    pub fn map<F>(&self, f: F) -> DateTimeTz
    where
        F: FnOnce(chrono::DateTime<chrono_tz::Tz>) -> chrono::DateTime<chrono_tz::Tz>,
    {
        DateTimeTz(f(self.0))
    }
}

impl std::str::FromStr for DateTimeTz {
    type Err = chrono::ParseError;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        let v: Vec<&str> = s.split_terminator(' ').collect();
        if v.len() == 2 {
            let tz = v[1].parse::<chrono_tz::Tz>().unwrap();
            chrono::DateTime::parse_from_rfc3339(v[0]).map(|ts| DateTimeTz(ts.with_timezone(&tz)))
        } else {
            chrono::DateTime::parse_from_rfc3339(v[0]).map(|ts| DateTimeTz(ts.with_timezone(&UTC)))
        }
    }
}

impl From<chrono::DateTime<chrono_tz::Tz>> for DateTimeTz {
    fn from(dt: chrono::DateTime<chrono_tz::Tz>) -> DateTimeTz {
        DateTimeTz(dt)
    }
}

struct DateTimeTzVisitor;

impl<'de> Visitor<'de> for DateTimeTzVisitor {
    type Value = DateTimeTz;

    fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
        formatter.write_str("a string date time representation that can be parsed")
    }

    fn visit_str<E: de::Error>(self, s: &str) -> Result<Self::Value, E> {
        DateTimeTz::from_str(s).or(Err(E::custom(
            "string is not a parsable datetime representation".to_owned(),
        )))
    }
}

impl Serialize for DateTimeTz {
    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
        serializer.serialize_str(&self.to_string())
    }
}

impl<'de> Deserialize<'de> for DateTimeTz {
    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
        deserializer.deserialize_str(DateTimeTzVisitor)
    }
}

#[derive(Clone, Debug, Serialize, Deserialize)]
struct Steps {
    date: DateTimeTz,
    steps: u32,
}

impl From<Steps> for ft_core::Steps {
    fn from(s: Steps) -> Self {
        Self {
            date: s.date.0.naive_utc().date(),
            count: s.steps,
        }
    }
}

#[derive(Clone, Debug, Serialize, Deserialize)]
struct Weight {
    date: DateTimeTz,
    weight: f64,
}

impl From<Weight> for ft_core::Weight {
    fn from(w: Weight) -> Self {
        Self {
            date: w.date.0.naive_utc().date(),
            weight: w.weight * si::KG,
        }
    }
}

#[derive(Clone, Debug, Serialize, Deserialize)]
enum TDActivity {
    Cycling,
    Rowing,
    Running,
    Swimming,
    Walking,
}

impl From<TDActivity> for ft_core::TimeDistanceActivity {
    fn from(activity: TDActivity) -> Self {
        match activity {
            TDActivity::Cycling => ft_core::TimeDistanceActivity::Biking,
            TDActivity::Rowing => ft_core::TimeDistanceActivity::Rowing,
            TDActivity::Running => ft_core::TimeDistanceActivity::Running,
            TDActivity::Swimming => ft_core::TimeDistanceActivity::Swimming,
            TDActivity::Walking => ft_core::TimeDistanceActivity::Walking,
        }
    }
}

#[derive(Clone, Debug, Serialize, Deserialize)]
struct TimeDistance {
    date: DateTimeTz,
    activity: TDActivity,
    comments: Option<String>,
    distance: Option<f64>,
    duration: Option<f64>,
}

impl From<TimeDistance> for ft_core::TimeDistance {
    fn from(td: TimeDistance) -> Self {
        Self {
            datetime: td.date.0.fixed_offset(),
            activity: td.activity.into(),
            comments: td.comments,
            distance: td.distance.map(|d| d * si::M),
            duration: td.duration.map(|d| d * si::S),
        }
    }
}

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

impl From<SRActivity> for SetRepActivity {
    fn from(activity: SRActivity) -> Self {
        match activity {
            SRActivity::Pushups => Self::Pushups,
            SRActivity::Situps => Self::Situps,
        }
    }
}

#[derive(Clone, Debug, Serialize, Deserialize)]
struct SetRep {
    date: DateTimeTz,
    activity: SRActivity,
    sets: Vec<u32>,
    comments: Option<String>,
}

impl From<SetRep> for ft_core::SetRep {
    fn from(sr: SetRep) -> Self {
        Self {
            date: sr.date.0.naive_utc().date(),
            activity: sr.activity.into(),
            sets: sr.sets,
            comments: sr.comments,
        }
    }
}

#[derive(Clone, Debug, Serialize, Deserialize)]
enum RDActivity {
    MartialArts,
}

impl From<RDActivity> for DurationWorkoutActivity {
    fn from(_: RDActivity) -> Self {
        Self::MartialArts
    }
}

#[derive(Clone, Debug, Serialize, Deserialize)]
struct RepDuration {
    date: DateTimeTz,
    activity: RDActivity,
    sets: Vec<f64>,
}

impl From<RepDuration> for DurationWorkout {
    fn from(rd: RepDuration) -> Self {
        Self {
            datetime: rd.date.0.fixed_offset(),
            activity: rd.activity.into(),
            duration: rd.sets.into_iter().map(|d| d * si::S).next(),
            comments: None,
        }
    }
}

#[derive(Clone, Debug, Serialize, Deserialize)]
enum LegacyData {
    RepDuration(RepDuration),
    SetRep(SetRep),
    Steps(Steps),
    TimeDistance(TimeDistance),
    Weight(Weight),
}

#[derive(Clone, Debug, Serialize, Deserialize)]
struct LegacyRecord {
    id: RecordId,
    data: LegacyData,
}

impl From<LegacyRecord> for Record<TraxRecord> {
    fn from(record: LegacyRecord) -> Self {
        match record.data {
            LegacyData::RepDuration(rd) => Record {
                id: record.id,
                data: TraxRecord::DurationWorkout(rd.into()),
            },
            LegacyData::SetRep(sr) => Record {
                id: record.id,
                data: TraxRecord::SetRep(sr.into()),
            },
            LegacyData::Steps(s) => Record {
                id: record.id,
                data: TraxRecord::Steps(s.into()),
            },
            LegacyData::TimeDistance(td) => Record {
                id: record.id,
                data: TraxRecord::TimeDistance(td.into()),
            },
            LegacyData::Weight(weight) => Record {
                id: record.id,
                data: TraxRecord::Weight(weight.into()),
            },
        }
    }
}

fn main() {
    let mut args = std::env::args();
    let _ = args.next().unwrap();
    let input_filename = args.next().unwrap();
    println!("input filename: {}", input_filename);
    // let output: Series<ft_core::TraxRecord> = Series::open_file("import.fitnesstrax").unwrap();

    let input_file = File::open(input_filename).unwrap();
    let mut buf_reader = BufReader::new(input_file);
    // let mut contents = String::new();
    // buf_reader.read_(&mut contents).unwrap();

    let mut count = 0;

    loop {
        let mut line = String::new();
        let res = buf_reader.read_line(&mut line);
        match res {
            Err(err) => {
                panic!("failed after {} lines: {:?}", count, err);
            }
            Ok(0) => std::process::exit(0),
            Ok(_) => {
                let record = serde_json::from_str::<LegacyRecord>(&line).unwrap();
                let record: Record<TraxRecord> = record.into();
                println!("{}", serde_json::to_string(&record).unwrap());
                count += 1;
            }
        }
    }
}
