mist-core 2.0.1

core functionality of mist
use crate::{error::Result, timer::Run};
use ron::{
    de::from_str,
    ser::{to_string_pretty, to_writer_pretty, PrettyConfig},
};
use serde::Deserialize;
use std::{
    io::{BufRead, BufReader, Read, Write},
    str::FromStr,
};

#[derive(Deserialize)]
struct LegacyRun {
    game_title: String,
    category: String,
    offset: Option<u128>,
    pb: u128,
    splits: Vec<String>,
    pb_times: Vec<u128>,
    gold_times: Vec<u128>,
}

#[derive(Deserialize)]
struct RunV1 {
    game_title: String,
    category: String,
    offset: Option<u128>,
    pb: u128,
    splits: Vec<String>,
    pb_times: Vec<u128>,
    gold_times: Vec<u128>,
    sum_times: Vec<(u128, u128)>,
}

impl From<LegacyRun> for Run {
    fn from(r: LegacyRun) -> Run {
        Run::new(
            r.category,
            r.game_title,
            r.offset.into(),
            r.pb.into(),
            &r.splits,
            &r.pb_times.iter().map(|&t| t.into()).collect::<Vec<_>>(),
            &r.gold_times.iter().map(|&t| t.into()).collect::<Vec<_>>(),
            &r.pb_times
                .iter()
                .map(|&t| (1u128, t.into()))
                .collect::<Vec<_>>(),
        )
    }
}

impl From<RunV1> for Run {
    fn from(r: RunV1) -> Run {
        Run::new(
            r.category,
            r.game_title,
            r.offset.into(),
            r.pb.into(),
            &r.splits,
            &r.pb_times.iter().map(|&t| t.into()).collect::<Vec<_>>(),
            &r.gold_times.iter().map(|&t| t.into()).collect::<Vec<_>>(),
            &r.sum_times
                .iter()
                .map(|&(n, t)| (n, t.into()))
                .collect::<Vec<_>>(),
        )
    }
}

impl Run {
    pub fn from_reader_msf(msf: impl Read) -> Result<Self> {
        let r = BufReader::new(msf);
        Self::parse_impl(r.lines().map_while(|r| r.ok()))
    }

    pub fn from_str_msf(msf: &str) -> Result<Self> {
        Self::parse_impl(msf.lines())
    }

    pub fn to_writer(&self, mut writer: impl Write) -> Result<()> {
        let run = super::sanify_run(self);
        writer.write_all(b"version 2\n")?;
        to_writer_pretty(&mut writer, &run, PrettyConfig::new())?;
        Ok(())
    }

    pub fn to_string(&self) -> Result<String> {
        let run = super::sanify_run(self);
        let mut s = String::from("version 2\n");
        s.push_str(&to_string_pretty(&run, PrettyConfig::new())?);
        Ok(s)
    }

    fn parse_impl<S: AsRef<str>>(mut lines: impl Iterator<Item = S>) -> Result<Self> {
        // TODO: better error handling
        let ver_info = String::from_str(lines.next().ok_or("Input was empty.")?.as_ref()).unwrap();
        let version: u32 = match ver_info.rsplit_once(' ') {
            Some(num) => num.1.parse::<u32>().unwrap_or(0),
            None => 0,
        };
        let data = {
            let mut s = String::new();
            if version == 0 {
                s.push_str(&ver_info);
            }
            for line in lines {
                s.push_str(line.as_ref());
                s.push('\n');
            }
            s
        };
        let run = match version {
            1 => from_str::<RunV1>(&data)?.into(),
            2 => from_str::<Run>(&data)?,
            _ => from_str::<LegacyRun>(&data)?.into(),
        };
        Ok(super::sanify_run(&run))
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::timer::TimeType::{self, *};

    const V2_RUN: &str = "version 2\n
        (
            game_title: \"test\",
            category: \"test\",
            offset: Time(200),
            pb: Time(1234),
            splits: [\"test\"],
            pb_times: [Skipped(1234)],
            gold_times: [Time(1234)],
            sum_times: [(2, None)],
        )";

    #[test]
    fn test_parse_v2() {
        assert_eq!(
            Run::from_str_msf(V2_RUN).unwrap(),
            Run::new(
                "test",
                "test",
                Time(200),
                Time(1234),
                &["test".into()],
                &[Skipped(1234)],
                &[Time(1234)],
                &[(2, TimeType::None)]
            )
        );
    }

    const V1_RUN: &str = "version 1\n
        (
            game_title: \"test\",
            category: \"test\",
            offset: Some(200),
            pb: 1234,
            splits: [\"test\"],
            pb_times: [1234],
            gold_times: [1234],
            sum_times: [(2, 2480)],
        )";

    #[test]
    fn test_parse_v1() {
        assert_eq!(
            Run::from_str_msf(V1_RUN).unwrap(),
            Run::new(
                "test",
                "test",
                Time(200),
                Time(1234),
                &["test".into()],
                &[Time(1234)],
                &[Time(1234)],
                &[(2, Time(2480))]
            )
        );
    }

    const LEGACY_RUN: &str = "(
        game_title: \"test\",
        category: \"test\",
        offset: Some(200),
        pb: 1234,
        splits: [\"test\"],
        pb_times: [1234],
        gold_times: [1234],
    )";

    #[test]
    fn test_parse_legacy() {
        assert_eq!(
            Run::from_str_msf(LEGACY_RUN).unwrap(),
            Run::new(
                "test",
                "test",
                Time(200),
                Time(1234),
                &["test".into()],
                &[Time(1234)],
                &[Time(1234)],
                &[(1, Time(1234))]
            )
        );
    }

    const INSANE_RUN: &str = "version 1\n
        (
            game_title: \"test\",
            category: \"test\",
            offset: Some(200),
            pb: 1234,
            splits: [\"test\", \"test2\"],
            pb_times: [1234],
            gold_times: [1234],
            sum_times: [(2, 1234), (1, 1243), (5, 420)],
        )";

    #[test]
    fn test_sanity_check() {
        let run = Run::from_str_msf(INSANE_RUN).unwrap();
        let run = crate::parse::sanify_run(&run);
        assert_eq!(
            run,
            Run::new(
                "test",
                "test",
                Time(200),
                Time(1234),
                &["test".into(), "test2".into()],
                &[Time(1234), TimeType::None],
                &[Time(1234), TimeType::None],
                &[(2, Time(1234)), (1, Time(1243))]
            )
        );
    }
}