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
//! Convenience functions for formatting and parsing parameter values in various common formats.
//!
//! Functions prefixed with `v2s_` are meant to be used with the `.value_to_string()` parameter
//! functions, while the `s2v_` functions are meant to be used wit the `.string_to_value()`.
//! functions. Most of these formatters come as a pair. Check each formatter's documentation for any
//! additional usage information.

use std::cmp::Ordering;
use std::sync::Arc;

use crate::util;

// TODO: The v2s and s2v naming convention isn't ideal, but at least it's unambiguous. Is there a
//       better way to name these functions? Should we just split this up into two modules?

/// Round an `f32` value to always have a specific number of decimal digits. Avoids returning
/// negative zero values to make sure string->value->string roundtrips work correctly. Otherwise
/// `-0.001` rounded to two digits would result in `-0.00`.
pub fn v2s_f32_rounded(digits: usize) -> Arc<dyn Fn(f32) -> String + Send + Sync> {
    let rounding_multiplier = 10u32.pow(digits as u32) as f32;
    Arc::new(move |value| {
        // See above
        if (value * rounding_multiplier).round() / rounding_multiplier == 0.0 {
            format!("{:.digits$}", 0.0)
        } else {
            format!("{value:.digits$}")
        }
    })
}

/// Format a `[0, 1]` number as a percentage. Does not include the percent sign, you should specify
/// this as the parameter's unit.
pub fn v2s_f32_percentage(digits: usize) -> Arc<dyn Fn(f32) -> String + Send + Sync> {
    Arc::new(move |value| format!("{:.digits$}", value * 100.0))
}

/// Parse a `[0, 100]` percentage to a `[0, 1]` number. Handles the percentage unit for you. Used in
/// conjunction with [`v2s_f32_percentage()`].
pub fn s2v_f32_percentage() -> Arc<dyn Fn(&str) -> Option<f32> + Send + Sync> {
    Arc::new(|string| {
        string
            .trim_end_matches(&[' ', '%'])
            .parse()
            .ok()
            .map(|x: f32| x / 100.0)
    })
}

/// Format a positive number as a compression ratio. A value of 4 will be formatted as `4.0:1` while
/// 0.25 is formatted as `1:4.0`.
pub fn v2s_compression_ratio(digits: usize) -> Arc<dyn Fn(f32) -> String + Send + Sync> {
    Arc::new(move |value| {
        if value >= 1.0 {
            format!("{value:.digits$}:1")
        } else {
            format!("1:{:.digits$}", value.recip())
        }
    })
}

/// Parse a `x:y` compression ratio back to a floating point number. Used in conjunction with
/// [`v2s_compression_ratio()`]. Plain numbers are parsed directly for UX's sake.
pub fn s2v_compression_ratio() -> Arc<dyn Fn(&str) -> Option<f32> + Send + Sync> {
    Arc::new(|string| {
        let string = string.trim();
        string
            .trim()
            .split_once(':')
            .and_then(|(numerator, denominator)| {
                let numerator: f32 = numerator.trim().parse().ok()?;
                let denominator: f32 = denominator.trim().parse().ok()?;

                Some(numerator / denominator)
            })
            // Just parse the value directly if it doesn't contain a colon
            .or_else(|| string.parse().ok())
    })
}

/// Turn an `f32` value from voltage gain to decibels using the semantics described in
/// [`util::gain_to_db()]. You should use either `" dB"` or `" dBFS"` for the parameter's unit.
/// `0.0` will be formatted as `-inf`.
pub fn v2s_f32_gain_to_db(digits: usize) -> Arc<dyn Fn(f32) -> String + Send + Sync> {
    Arc::new(move |value| {
        if value < util::MINUS_INFINITY_GAIN {
            String::from("-inf")
        } else {
            // Never print -0.0 since that just looks weird and confusing
            let value_db = util::gain_to_db(value);
            let value_db = if value_db.abs() < 1e-6 { 0.0 } else { value_db };

            format!("{value_db:.digits$}")
        }
    })
}

/// Parse a decibel value to a linear voltage gain ratio. Handles the `dB` or `dBFS` units for you.
/// Used in conjunction with [`v2s_f32_gain_to_db()`]. `-inf dB` will be parsed to 0.0.
pub fn s2v_f32_gain_to_db() -> Arc<dyn Fn(&str) -> Option<f32> + Send + Sync> {
    Arc::new(|string| {
        let string = string.trim_end_matches(&[' ', 'd', 'D', 'b', 'B', 'f', 'F', 's', 'S']);
        // NOTE: The above line strips the `f`, so checked for `-inf` here will always return false
        if string.eq_ignore_ascii_case("-in") {
            Some(0.0)
        } else {
            string.parse().ok().map(util::db_to_gain)
        }
    })
}

/// Turn an `f32` `[-1, 1]` value to a panning value where negative values are represented by
/// `[100L, 1L]`, 0 gets turned into `C`, and positive values become `[1R, 100R]` values.
pub fn v2s_f32_panning() -> Arc<dyn Fn(f32) -> String + Send + Sync> {
    Arc::new(move |value| match value.partial_cmp(&0.0) {
        Some(Ordering::Less) => format!("{:.0}L", value * -100.0),
        Some(Ordering::Equal) => String::from("C"),
        Some(Ordering::Greater) => format!("{:.0}R", value * 100.0),
        None => String::from("NaN"),
    })
}

/// Parse a pan value in the format of [`v2s_f32_panning()] to a linear value in the range `[-1,
/// 1]`.
pub fn s2v_f32_panning() -> Arc<dyn Fn(&str) -> Option<f32> + Send + Sync> {
    Arc::new(|string| {
        let string = string.trim();
        let cleaned_string = string
            .trim_end_matches(&[' ', 'l', 'L', 'c', 'C', 'r', 'R'])
            .parse()
            .ok();
        match string.chars().last()?.to_uppercase().next()? {
            'L' => cleaned_string.map(|x: f32| x / -100.0),
            'C' => Some(0.0),
            'R' => cleaned_string.map(|x: f32| x / 100.0),
            _ => None,
        }
    })
}

/// Format a `f32` Hertz value as a rounded `Hz` below 1000 Hz, and as a rounded `kHz` value above
/// 1000 Hz. This already includes the unit.
pub fn v2s_f32_hz_then_khz(digits: usize) -> Arc<dyn Fn(f32) -> String + Send + Sync> {
    Arc::new(move |value| {
        if value < 1000.0 {
            format!("{value:.digits$} Hz")
        } else {
            format!("{:.digits$} kHz", value / 1000.0, digits = digits.max(1))
        }
    })
}

/// [`v2s_f32_hz_then_khz()`], but also includes the note name. Can be used with
/// [`s2v_f32_hz_then_khz()`].
pub fn v2s_f32_hz_then_khz_with_note_name(
    digits: usize,
    include_cents: bool,
) -> Arc<dyn Fn(f32) -> String + Send + Sync> {
    Arc::new(move |value| {
        // With 0.0 this would result in a subtraction below i32's minimum value, and it would look
        // ridiculous anyways so we'll just not even bother for tiny values
        if value.abs() < 1.0 {
            return format!("{value:.digits$} Hz");
        }

        // This is the inverse of the formula in `f32_midi_note_to_freq`
        let fractional_note = util::freq_to_midi_note(value);
        let note = fractional_note.round();
        let cents = ((fractional_note - note) * 100.0).round() as i32;

        let note_name = util::NOTES[(note as i32).rem_euclid(12) as usize];
        // NOTE: This is different compared from `(note as i32 / 12) - 1` because truncating always
        //       rounds towards zero
        let octave = (note / 12.0).floor() as i32 - 1;
        let note_str = if cents == 0 || !include_cents {
            format!("{note_name}{octave}")
        } else {
            format!("{note_name}{octave}, {cents:+} ct.")
        };

        if value < 1000.0 {
            format!("{value:.digits$} Hz, {note_str}")
        } else {
            format!(
                "{:.digits$} kHz, {}",
                value / 1000.0,
                note_str,
                digits = digits.max(1)
            )
        }
    })
}

/// Convert an input in the same format at that of [`v2s_f32_hz_then_khz()] to a Hertz value. This
/// additionally also accepts note names in the same format as [`s2v_i32_note_formatter()`], and
/// optionally also with cents in the form of `D#5, -23 ct.`.
pub fn s2v_f32_hz_then_khz() -> Arc<dyn Fn(&str) -> Option<f32> + Send + Sync> {
    // FIXME: This is a very crude way to reuse the note value formatter. There's no real runtime
    //        penalty for doing it this way, but it does look less pretty.
    let note_formatter = s2v_i32_note_formatter();

    Arc::new(move |string| {
        let string = string.trim();

        // The input can contain a frequency in Hz or kHz, a note name, a note name and cents, or
        // one of those two combined with a frequency. In the last case we'll ignore the frequency.
        // If the string cannot be parsed as a note name, we'll try parsing it as a frequency
        // instead. This is needed for the formatting roundtrip to work correctly. The input will
        // consists of 1 to three segments, so we'll try to unpack them like this so we can pattern
        // match on them
        let mut segments = string.split(',');
        let segments = (segments.next(), segments.next(), segments.next());

        if let (_, Some(midi_note_number_str), Some(cents_str))
        | (Some(midi_note_number_str), Some(cents_str), None) = segments
        {
            let cents_str = cents_str
                .trim_start_matches([' ', '+'])
                .trim_end_matches([' ', 'C', 'c', 'E', 'e', 'N', 'n', 'T', 't', 'S', 's', '.']);

            if let (Some(midi_note_number), Ok(cents)) = (
                note_formatter(midi_note_number_str),
                cents_str.parse::<i32>(),
            ) {
                let plain_note_freq = util::f32_midi_note_to_freq(midi_note_number as f32);
                let cents_multiplier = 2.0f32.powf(cents as f32 / 100.0 / 12.0);
                return Some(plain_note_freq * cents_multiplier);
            }
        }

        if let (_, Some(midi_note_number_str), _) | (Some(midi_note_number_str), None, None) =
            segments
        {
            if let Some(midi_note_number) = note_formatter(midi_note_number_str) {
                return Some(util::f32_midi_note_to_freq(midi_note_number as f32));
            }
        }

        // Otherwise we'll accept values in either Hz (with or without unit) or kHz
        let frequency_segment = segments.0?;
        let cleaned_string = frequency_segment
            .trim_end_matches([' ', 'k', 'K', 'h', 'H', 'z', 'Z'])
            .parse()
            .ok();
        match frequency_segment.get(frequency_segment.len().saturating_sub(3)..) {
            Some(unit) if unit.eq_ignore_ascii_case("khz") => cleaned_string.map(|x| x * 1000.0),
            // Even if there's no unit at all, just assume the input is in Hertz
            _ => cleaned_string,
        }
    })
}

/// Format an order/power of two. Useful in conjunction with [`s2v_i32_power_of_two()`] to limit
/// integer parameter ranges to be only powers of two.
pub fn v2s_i32_power_of_two() -> Arc<dyn Fn(i32) -> String + Send + Sync> {
    Arc::new(|value| format!("{}", 1 << value))
}

/// Parse a parameter input string to a power of two. Useful in conjunction with
/// [`v2s_i32_power_of_two()`] to limit integer parameter ranges to be only powers of two.
pub fn s2v_i32_power_of_two() -> Arc<dyn Fn(&str) -> Option<i32> + Send + Sync> {
    Arc::new(|string| string.parse().ok().map(|n: i32| (n as f32).log2() as i32))
}

/// Turns an integer MIDI note number (usually in the range [0, 127]) into a note name, where 60 is
/// C4 and 69 is A4 (nice).
pub fn v2s_i32_note_formatter() -> Arc<dyn Fn(i32) -> String + Send + Sync> {
    Arc::new(move |value| {
        let note_name = util::NOTES[value.rem_euclid(12) as usize];
        let octave = (value / 12) - 1;
        format!("{note_name}{octave}")
    })
}

/// Parse a note name to a MIDI number using the inverse mapping from [`v2s_i32_note_formatter()].
pub fn s2v_i32_note_formatter() -> Arc<dyn Fn(&str) -> Option<i32> + Send + Sync> {
    Arc::new(|string| {
        let string = string.trim();
        if string.len() < 2 {
            return None;
        }

        // A valid trimmed string will either be be at least two characters (we already checked the
        // length) or at least three characters if the second character is a hash, and there may be
        // spaces in between the note name and the octave number
        let (note_name, octave) = string
            .split_once(|c: char| c.is_whitespace())
            .unwrap_or_else(|| {
                // Sharps need to be handled separately
                if string.len() > 2 && &string[1..2] == "#" {
                    (&string[..2], &string[2..])
                } else {
                    (&string[..1], &string[1..])
                }
            });

        let note_id = util::NOTES
            .iter()
            .position(|&candidate| note_name.eq_ignore_ascii_case(candidate))?
            as i32;
        let octave: i32 = octave.trim().parse().ok()?;

        // 0 = C-1, 12 = C0, 24 = C1
        Some(note_id + (12 * (octave + 1)))
    })
}

/// Display 'Bypassed' or 'Not Bypassed' depending on whether the parameter is true or false.
/// 'Enabled' would have also been a possibility here, but that could be a bit confusing.
pub fn v2s_bool_bypass() -> Arc<dyn Fn(bool) -> String + Send + Sync> {
    Arc::new(move |value| {
        if value {
            String::from("Bypassed")
        } else {
            String::from("Not Bypassed")
        }
    })
}

/// Parse a string in the same format as [`v2s_bool_bypass()].
pub fn s2v_bool_bypass() -> Arc<dyn Fn(&str) -> Option<bool> + Send + Sync> {
    Arc::new(|string| {
        let string = string.trim();
        if string.eq_ignore_ascii_case("bypassed") {
            Some(true)
        } else if string.eq_ignore_ascii_case("not bypassed") {
            Some(false)
        } else {
            None
        }
    })
}

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

    /// The rounding function should never return strings containing negative zero values.
    #[test]
    fn v2s_f32_rounded_negative_zero() {
        let v2s = v2s_f32_rounded(2);

        assert_eq!("0.00", v2s(-0.001));

        // Sanity check
        assert_eq!("-0.01", v2s(-0.009));
        assert_eq!("0.01", v2s(0.009));
    }

    // More of these validators could use tests, but this one in particular is tricky and I noticed
    // an issue where it didn't roundtrip correctly
    #[test]
    fn f32_hz_then_khz_with_note_name_roundtrip() {
        let v2s = v2s_f32_hz_then_khz_with_note_name(1, true);
        let s2v = s2v_f32_hz_then_khz();

        for freq in [0.0, 5.0, 7.18, 8.18, 69.420, 18181.8, 133333.7] {
            let string = v2s(freq);
            // We can't compare `freq` and `roundtrip_freq` because the string is rounded on both
            // cents and frequency and is thus lossy
            let roundtrip_freq = s2v(&string).unwrap();
            let roundtrip_string = v2s(roundtrip_freq);
            assert_eq!(
                string, roundtrip_string,
                "Unexpected: {string} -> {roundtrip_freq} -> {roundtrip_string}"
            );
        }
    }
}