1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
use std::io::{BufRead, BufReader, Lines, Read};
use std::str::FromStr;

use zip::read::ZipFile;
use zip::result::ZipError;

use crate::record::{self, Record};

pub struct Parser<R> {
    lines: Lines<BufReader<R>>,
}

impl<R> Parser<R> {
    pub fn new(rd: R) -> Result<Self, ParseError>
    where
        R: Read,
    {
        let mut lines = BufReader::new(rd).lines();

        let file_type = lines.next().ok_or(ParseError::InvalidFileType)??;
        if file_type != "FileType=text/acmi/tacview"
            && file_type != "\u{feff}FileType=text/acmi/tacview"
        {
            return Err(ParseError::InvalidFileType);
        }

        let version = lines.next().ok_or(ParseError::InvalidVersion)??;
        if version.get(..version.len() - 1) != Some("FileVersion=2.")
            || !version
                .get(version.len() - 1..)
                .map(|s| s.chars().all(|c| c.is_ascii_digit()))
                .unwrap_or(false)
        {
            return Err(ParseError::InvalidVersion);
        }

        Ok(Parser { lines })
    }

    pub fn new_compressed(rd: &mut R) -> Result<Parser<ZipFile<'_>>, ParseError>
    where
        R: Read,
    {
        let file = zip::read::read_zipfile_from_stream(rd)?
            .ok_or(ParseError::Zip(ZipError::FileNotFound))?;
        dbg!(file.name());
        Parser::new(file)
    }
}

impl<R> Iterator for Parser<R>
where
    R: Read,
{
    type Item = Result<Record, ParseError>;

    fn next(&mut self) -> Option<Self::Item> {
        loop {
            let next = self
                .lines
                .next()
                .filter(|r| r.as_ref().map(|l| !l.is_empty()).unwrap_or(true))?
                .map_err(ParseError::Io)
                .and_then(parse_line)
                .transpose();
            if next.is_some() {
                return next;
            }
        }
    }
}

fn parse_line(line: String) -> Result<Option<Record>, ParseError> {
    let mut chars = line.chars();
    match chars.next().ok_or(ParseError::Eol)? {
        '-' => {
            let id = u64::from_str_radix(&line[1..], 16)?;
            Ok(Some(Record::Remove(id)))
        }
        '#' => {
            let id = f64::from_str(&line[1..])?;
            Ok(Some(Record::Frame(id)))
        }
        '/' if chars.next() == Some('/') => Ok(None),
        _ => {
            let (id, rest) = line.split_once(',').ok_or(ParseError::Eol)?;

            Ok(Some(if id == "0" {
                let (name, value) = rest
                    .split_once('=')
                    .ok_or(ParseError::MissingDelimiter('='))?;
                if name == "Event" {
                    Record::Event(record::Event::from_str(value)?)
                } else {
                    Record::GlobalProperty(record::GlobalProperty::from_str(rest)?)
                }
            } else {
                Record::Update(record::Update::from_str(&line)?)
            }))
        }
    }
}

// TODO: line and position information for certain errors?
#[derive(Debug, thiserror::Error)]
pub enum ParseError {
    #[error("input is not a ACMI file")]
    InvalidFileType,
    #[error("invalid version, expected ACMI v2.x")]
    InvalidVersion,
    #[error("error reading input")]
    Io(#[from] std::io::Error),
    #[error("unexpected end of line")]
    Eol,
    #[error("object id is not a u64")]
    InvalidId(#[from] std::num::ParseIntError),
    #[error("expected numeric")]
    InvalidNumeric(#[from] std::num::ParseFloatError),
    #[error("could not find expected delimiter `{0}`")]
    MissingDelimiter(char),
    #[error("failed to parse event")]
    InvalidEvent,
    #[error("encountered invalid coordinate format")]
    InvalidCoordinateFormat,
    #[error("error reading zip compressed input")]
    Zip(#[from] zip::result::ZipError),
}