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
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
//! The [bmson format](https://bmson-spec.readthedocs.io/en/master/doc/index.html) definition.
//!
//! # Order of Processing
//!
//! When there are coincident events in the same pulse, they are processed in the order below:
//!
//! - [`Note`] and [`BgaEvent`] (are independent each other),
//! - [`BpmEvent`],
//! - [`StopEvent`].
//!
//! If a [`BpmEvent`] and a [`StopEvent`] appear on the same pulse, the current BPM will be changed at first, then scrolling the chart will be stopped for a while depending the changed BPM.
//!
//! If a [`Note`] and a [`StopEvent`] appear on the same pulse, the sound will be played (or should be hit by a player), then scrolling the chart will be stopped.
//!
//! # Layered Notes
//!
//! In case that notes (not BGM) from different sound channels exist on the same (key and pulse) position:
//!
//! - When its length is not equal to each other, yo should treat as an error and warn to a player.
//! - Otherwise your player may fusion the notes. That means when a player hit the key, two sounds will be played.
//!
//! # Differences from BMS
//!
//! - BMS can play different sound on the start and end of long note. But bmson does not allow this.
//! - Transparent color on BGA is not supported. But you can use PNG files having RGBA channels.

use std::{collections::HashMap, num::NonZeroU8};

use serde::{Deserialize, Serialize};
use thiserror::Error;

use crate::{
    lex::command::{JudgeLevel, Key},
    parse::{notes::BgaLayer, Bms},
    time::{ObjTime, Track},
};

use self::{
    fin_f64::FinF64,
    pulse::{PulseConverter, PulseNumber},
};

pub mod fin_f64;
pub mod pulse;

/// Top-level object for bmson format.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Bmson {
    /// Version of bmson format, which should be compared using [Semantic Version 2.0.0](http://semver.org/spec/v2.0.0.html). Older bmson file may not have this field, but lacking this must be an error.
    pub version: String,
    /// Score metadata.
    pub info: BmsonInfo,
    /// Location of bar lines in pulses. If `None`, then a 4/4 beat is assumed and bar lines will be generates every 4 quarter notes. If `Some(vec![])`, this chart will not have any bar line.
    ///
    /// This format represents an irregular meter by bar lines.
    pub lines: Option<Vec<BarLine>>,
    /// Events of bpm change. If there are coincident events, the successor is only applied.
    #[serde(default)]
    pub bpm_events: Vec<BpmEvent>,
    /// Events of scroll stop. If there are coincident events, they are happened in succession.
    #[serde(default)]
    pub stop_events: Vec<StopEvent>,
    /// Note data.
    pub sound_channels: Vec<SoundChannel>,
    /// BGA data.
    pub bga: Bga,
}

/// Header metadata of chart.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct BmsonInfo {
    /// Self explanatory title.
    pub title: String,
    /// Self explanatory subtitle. Usually this is shown as a smaller text than `title`.
    #[serde(default)]
    pub subtitle: String,
    /// Author of the chart. It may multiple names such as `Alice vs Bob`, `Alice feat. Bob` and so on. But you should respect the value because it usually have special meaning.
    pub artist: String,
    /// Other authors of the chart. This is useful for indexing and searching.
    ///
    /// Value of the array has form of `key:value`. The `key` can be `music`, `vocal`, `chart`, `image`, `movie` or `other`. If it has no `key`, you should treat as that `key` equals to `other`. The value may contains the spaces before and after `key` and `value`, so you should trim them.
    ///
    /// # Example
    ///
    /// ```json
    /// "subartists": ["music:5argon", "music:encX", "chart:flicknote", "movie:5argon", "image:5argon"]
    /// ```
    #[serde(default)]
    pub subartists: Vec<String>,
    /// Self explanatory genre.
    pub genre: String,
    /// Hint for layout lanes, e.g. "beat-7k", "popn-5k", "generic-nkeys". Defaults to `"beat-7k"`.
    ///
    /// If you want to support many lane modes of BMS, you should check this to determine the layout for lanes. Also you can check all lane information in `sound_channels` for strict implementation.
    #[serde(default = "default_mode_hint")]
    pub mode_hint: String,
    /// Special chart name, e.g. "BEGINNER", "NORMAL", "HYPER", "FOUR DIMENSIONS".
    #[serde(default)]
    pub chart_name: String,
    /// Self explanatory level number. It is usually set with subjective rating by the author.
    pub level: u32,
    /// Initial BPM.
    pub init_bpm: FinF64,
    /// Relative judge width in percentage. The variation amount may different by BMS player. Larger is easier.
    #[serde(default = "default_percentage")]
    pub judge_rank: FinF64,
    /// Relative life bar gain in percentage. The variation amount may different by BMS player. Larger is easier.
    #[serde(default = "default_percentage")]
    pub total: FinF64,
    /// Background image file name. This should be displayed during the game play.
    pub back_image: Option<String>,
    /// Eyecatch image file name. This should be displayed during the chart is loading.
    pub eyecatch_image: Option<String>,
    /// Title image file name. This should be displayed before the game starts instead of title of the music.
    pub title_image: Option<String>,
    /// Banner image file name. This should be displayed in music select or result scene. The aspect ratio of image is usually 15:4.
    pub banner_image: Option<String>,
    /// Preview music file name. This should be played when this chart is selected in a music select scene.
    pub preview_music: Option<String>,
    /// Numbers of pulse per quarter note in 4/4 measure. You must check this because it affects the actual seconds of `PulseNumber`.
    #[serde(default = "default_resolution")]
    pub resolution: u32,
}

/// Default mode hint, beatmania 7 keys.
pub fn default_mode_hint() -> String {
    "beat-7k".into()
}

/// Default relative percentage, 100%.
pub fn default_percentage() -> FinF64 {
    FinF64::new(100.0).unwrap()
}

/// Default resolution pulses per quarter note in 4/4 measure, 240 pulses.
pub fn default_resolution() -> u32 {
    240
}

/// Event of bar line of the chart.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct BarLine {
    /// Pulse number to place the line.
    pub y: PulseNumber,
}

/// Note sound file and positions to be placed in the chart.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SoundChannel {
    /// Sound file path. If the extension is not specified or not supported, you can try search files about other extensions for fallback.
    ///
    /// BMS players are expected to support the audio containers below:
    ///
    /// - WAV (`.wav`),
    /// - OGG (`.ogg`),
    /// - Audio-only MPEG-4 (`.m4a`).
    ///
    /// BMS players are expected to support the audio codec below:
    ///
    /// - LPCM (Linear Pulse-Code Modulation),
    /// - Ogg Vorbis,
    /// - AAC (Advanced Audio Coding).
    pub name: String,
    /// Data of note to be placed.
    pub notes: Vec<Note>,
}

/// Sound note to ring a sound file.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Note {
    /// Lane information. The `Some` number represents the key to play, otherwise it is not playable (BGM) note.
    pub x: Option<NonZeroU8>,
    /// Position to be placed.
    pub y: PulseNumber,
    /// Length of pulses of the note. It will be a normal note if zero, otherwise a long note.
    pub l: u32,
    /// Continuation flag. It will continue to ring rest of the file when play if `true`, otherwise it will play from start.
    pub c: bool,
}

/// BPM change note.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct BpmEvent {
    /// Position to change BPM of the chart.
    pub y: PulseNumber,
    /// New BPM to be.
    pub bpm: FinF64,
}

/// Scroll stop note.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct StopEvent {
    /// Start position to scroll stop.
    pub y: PulseNumber,
    /// Stopping duration in pulses.
    pub duration: u32,
}

/// BGA data.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Bga {
    /// Pictures data for playing BGA.
    pub bga_header: Vec<BgaHeader>,
    /// Base picture sequence.
    pub bga_events: Vec<BgaEvent>,
    /// Layered picture sequence.
    pub layer_events: Vec<BgaEvent>,
    /// Picture sequence displayed when missed.
    pub poor_events: Vec<BgaEvent>,
}

/// Picture file information.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct BgaHeader {
    /// Self explanatory ID of picture.
    pub id: BgaId,
    /// Picture file name.
    pub name: String,
}

/// BGA note to display the picture.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct BgaEvent {
    /// Position to display the picture in pulses.
    pub y: PulseNumber,
    /// ID of picture to display.
    pub id: BgaId,
}

/// Picture id for [`Bga`].
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub struct BgaId(pub u32);

/// Errors on converting from `Bms` into `Bmson`.
#[derive(Debug, Error, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum BmsonConvertError {
    /// The initial BPM was infinity or NaN.
    #[error("header bpm was invalid value")]
    InvalidBpm,
    /// The total percentage was infinity or NaN.
    #[error("header total was invalid value")]
    InvalidTotal,
}

impl TryFrom<Bms> for Bmson {
    type Error = BmsonConvertError;

    fn try_from(value: Bms) -> Result<Self, Self::Error> {
        let converter = PulseConverter::new(&value.notes);

        let has_7keys = value
            .notes
            .all_notes()
            .any(|note| note.key.is_extended_key());

        const EASY_WIDTH: f64 = 21.0;
        const NORMAL_WIDTH: f64 = 18.0;
        const HARD_WIDTH: f64 = 15.0;
        const VERY_HARD_WIDTH: f64 = 8.0;
        let judge_rank = FinF64::new(match value.header.rank {
            Some(JudgeLevel::Easy) => EASY_WIDTH / NORMAL_WIDTH,
            Some(JudgeLevel::Normal) | None => 1.0,
            Some(JudgeLevel::Hard) => HARD_WIDTH / NORMAL_WIDTH,
            Some(JudgeLevel::VeryHard) => VERY_HARD_WIDTH / NORMAL_WIDTH,
        })
        .unwrap();

        let resolution = value.notes.resolution_for_pulses();

        let last_obj_time = value
            .notes
            .last_obj_time()
            .unwrap_or_else(|| ObjTime::new(0, 0, 4));
        let lines = (0..=last_obj_time.track.0)
            .map(|track| BarLine {
                y: converter.get_pulses_on(Track(track)),
            })
            .collect();

        let bpm_events = value
            .notes
            .bpm_changes()
            .values()
            .map(|bpm_change| {
                Ok(BpmEvent {
                    y: converter.get_pulses_at(bpm_change.time),
                    bpm: FinF64::new(bpm_change.bpm).ok_or(BmsonConvertError::InvalidBpm)?,
                })
            })
            .collect::<Result<Vec<_>, BmsonConvertError>>()?;

        let stop_events = value
            .notes
            .stops()
            .values()
            .map(|stop| StopEvent {
                y: converter.get_pulses_at(stop.time),
                duration: stop.duration,
            })
            .collect();

        let info = BmsonInfo {
            title: value.header.title.unwrap_or_default(),
            subtitle: value.header.subtitle.unwrap_or_default(),
            artist: value.header.artist.unwrap_or_default(),
            subartists: vec![value.header.sub_artist.unwrap_or_default()],
            genre: value.header.genre.unwrap_or_default(),
            mode_hint: if has_7keys {
                "beat-7k".into()
            } else {
                "beat-5k".into()
            },
            chart_name: "".into(),
            level: value.header.play_level.unwrap_or_default() as u32,
            init_bpm: FinF64::new(value.header.bpm.unwrap_or(120.0))
                .ok_or(BmsonConvertError::InvalidBpm)?,
            judge_rank,
            total: FinF64::new(value.header.total.unwrap_or(100.0))
                .ok_or(BmsonConvertError::InvalidTotal)?,
            back_image: value
                .header
                .back_bmp
                .as_ref()
                .cloned()
                .map(|path| path.display().to_string()),
            eyecatch_image: value
                .header
                .stage_file
                .map(|path| path.display().to_string()),
            title_image: value.header.back_bmp.map(|path| path.display().to_string()),
            banner_image: value.header.banner.map(|path| path.display().to_string()),
            preview_music: None,
            resolution,
        };

        let sound_channels = {
            let path_root = value.header.wav_path_root.unwrap_or_default();
            let mut sound_channels = HashMap::new();
            for note in value.notes.all_notes() {
                let note_lane = note
                    .kind
                    .is_playable()
                    .then_some(
                        match note.key {
                            Key::Key1 => 1,
                            Key::Key2 => 2,
                            Key::Key3 => 3,
                            Key::Key4 => 4,
                            Key::Key5 => 5,
                            Key::Key6 => 6,
                            Key::Key7 => 7,
                            Key::Scratch | Key::FreeZone => 8,
                        } + if note.is_player1 { 0 } else { 8 },
                    )
                    .map(|num| NonZeroU8::new(num).unwrap());
                let pulses = converter.get_pulses_at(note.offset);
                let duration =
                    if let Some(next_note) = value.notes.next_obj_by_key(note.key, note.offset) {
                        pulses.abs_diff(converter.get_pulses_at(next_note.offset))
                    } else {
                        0
                    };
                let to_insert = Note {
                    x: note_lane,
                    y: pulses,
                    l: duration,
                    c: false,
                };

                sound_channels
                    .entry(note.obj)
                    .and_modify(|channel: &mut SoundChannel| channel.notes.push(to_insert.clone()))
                    .or_insert_with(|| {
                        let sound_path = path_root.join(
                            value
                                .header
                                .wav_files
                                .get(&note.obj)
                                .cloned()
                                .unwrap_or_default(),
                        );
                        SoundChannel {
                            name: sound_path.display().to_string(),
                            notes: vec![to_insert],
                        }
                    });
            }
            sound_channels.into_values().collect()
        };

        let bga = {
            let mut bga = Bga {
                bga_header: vec![],
                bga_events: vec![],
                layer_events: vec![],
                poor_events: vec![],
            };
            for (id, bmp) in &value.header.bmp_files {
                bga.bga_header.push(BgaHeader {
                    id: BgaId(id.0.get() as u32),
                    name: bmp.file.display().to_string(),
                });
            }
            for (&time, change) in value.notes.bga_changes() {
                let target = match change.layer {
                    BgaLayer::Base => &mut bga.bga_events,
                    BgaLayer::Poor => &mut bga.poor_events,
                    BgaLayer::Overlay => &mut bga.layer_events,
                };
                target.push(BgaEvent {
                    y: converter.get_pulses_at(time),
                    id: BgaId(change.id.0.get() as u32),
                })
            }
            bga
        };

        Ok(Self {
            version: "1.0.0".into(),
            info,
            lines: Some(lines),
            bpm_events,
            stop_events,
            sound_channels,
            bga,
        })
    }
}