// Copyright 2026 California Institute of Technology
//
// You should have received a copy of the licensing terms for this
// software included in the file “LICENSE” located in the top-level
// directory of this package. If you did not, you can view a copy at
// http://dcc.ligo.org/M1500244/LICENSE.txt

//! StripTool `.stp` configuration parser.  Only the per-curve
//! `Strip.Curve.<N>.Name/.Min/.Max` keys are read; every other key is
//! ignored.  Channels come out ordered by curve index.

use crate::channel::{ChannelConfig, Range, is_valid_channel_name};
use std::collections::{BTreeMap, HashSet};
use std::path::Path;

/// Does this input look like a StripTool file?  Detected by the `.stp`
/// suffix or by `StripConfig` being the first bytes of the file.
pub fn is_stp(path: &Path, contents: &str) -> bool {
    path.extension()
        .is_some_and(|ext| ext.eq_ignore_ascii_case("stp"))
        || contents.starts_with("StripConfig")
}

/// Per-curve fields accumulated while scanning the file.
#[derive(Default)]
struct Curve {
    name: Option<String>,
    min: Option<f64>,
    max: Option<f64>,
}

/// Parse a StripTool `.stp` file into an ordered channel list.
///
/// Curves are keyed by index (`Strip.Curve.<N>.<attribute>`) and emitted
/// in index order.  A range attaches only when both Min and Max are
/// present, finite, and min < max; otherwise the channel has no range.
/// Invalid names and duplicate names are skipped.
pub fn parse_stp(input: &str) -> Vec<ChannelConfig> {
    let mut curves: BTreeMap<u32, Curve> = BTreeMap::new();
    for line in input.lines() {
        let mut words = line.split_whitespace();
        let (Some(key), Some(value)) = (words.next(), words.next()) else {
            continue;
        };
        let Some(rest) = key.strip_prefix("Strip.Curve.") else {
            continue;
        };
        let Some((index, attribute)) = rest.split_once('.') else {
            continue;
        };
        let Ok(index) = index.parse::<u32>() else {
            continue;
        };
        let curve = curves.entry(index).or_default();
        match attribute {
            "Name" => curve.name = Some(value.to_string()),
            "Min" => curve.min = value.parse().ok(),
            "Max" => curve.max = value.parse().ok(),
            _ => {}
        }
    }

    let mut seen = HashSet::new();
    let mut channels = Vec::new();
    for curve in curves.into_values() {
        let Some(name) = curve.name else {
            continue; // Min/Max without a Name names no channel
        };
        if !is_valid_channel_name(&name) || !seen.insert(name.clone()) {
            continue;
        }
        let range = curve
            .min
            .zip(curve.max)
            .and_then(|(min, max)| Range::new(min, max));
        channels.push(ChannelConfig::new(name, range));
    }
    channels
}

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

    const SPEC_SAMPLE: &str = "StripConfig                   1.2\n\
        Strip.Curve.0.Name            X2:CPU-USE\n\
        Strip.Curve.1.Name            X2:MEM-FREE\n\
        Strip.Curve.0.Min              0\n\
        Strip.Curve.1.Min              0\n\
        Strip.Curve.0.Max              100\n\
        Strip.Curve.1.Max              100\n";

    #[test]
    fn sample_input_from_spec() {
        assert_eq!(
            parse_stp(SPEC_SAMPLE),
            vec![
                ChannelConfig::new("X2:CPU-USE", Range::new(0.0, 100.0)),
                ChannelConfig::new("X2:MEM-FREE", Range::new(0.0, 100.0)),
            ]
        );
    }

    #[test]
    fn detected_by_suffix_or_leading_bytes() {
        assert!(is_stp(Path::new("channels.stp"), ""));
        assert!(is_stp(Path::new("channels.STP"), ""));
        assert!(is_stp(Path::new("channels.txt"), SPEC_SAMPLE));
        assert!(!is_stp(Path::new("channels.txt"), "X1:SINE_1\n"));
        // Not the first bytes -> not detected by content.
        assert!(!is_stp(
            Path::new("channels.txt"),
            "# note\nStripConfig 1.2\n"
        ));
    }

    #[test]
    fn curves_come_out_in_index_order() {
        let input = "Strip.Curve.2.Name X1:C\n\
                     Strip.Curve.0.Name X1:A\n\
                     Strip.Curve.10.Name X1:D\n\
                     Strip.Curve.1.Name X1:B\n";
        let names: Vec<_> = parse_stp(input).into_iter().map(|c| c.name).collect();
        assert_eq!(names, vec!["X1:A", "X1:B", "X1:C", "X1:D"]);
    }

    #[test]
    fn range_needs_both_valid_bounds() {
        let input = "Strip.Curve.0.Name X1:MIN_ONLY\n\
                     Strip.Curve.0.Min 0\n\
                     Strip.Curve.1.Name X1:MAX_ONLY\n\
                     Strip.Curve.1.Max 10\n\
                     Strip.Curve.2.Name X1:INVERTED\n\
                     Strip.Curve.2.Min 10\n\
                     Strip.Curve.2.Max 0\n\
                     Strip.Curve.3.Name X1:EQUAL\n\
                     Strip.Curve.3.Min 5\n\
                     Strip.Curve.3.Max 5\n\
                     Strip.Curve.4.Name X1:BAD_NUMBER\n\
                     Strip.Curve.4.Min zero\n\
                     Strip.Curve.4.Max 10\n";
        for channel in parse_stp(input) {
            assert_eq!(channel.range, None, "{}", channel.name);
        }
    }

    #[test]
    fn scientific_notation_bounds() {
        let input = "Strip.Curve.0.Name X1:A\n\
                     Strip.Curve.0.Min -1.000000e+02\n\
                     Strip.Curve.0.Max 1.000000e+02\n";
        assert_eq!(parse_stp(input)[0].range, Range::new(-100.0, 100.0));
    }

    #[test]
    fn unknown_keys_and_junk_are_ignored() {
        let input = "StripConfig 1.2\n\
                     Strip.Time.Timespan 300\n\
                     Strip.Curve.0.Name X1:A\n\
                     Strip.Curve.0.Units V\n\
                     Strip.Curve.0.Comment sensor\n\
                     Strip.Curve.0.Precision 4\n\
                     Strip.Curve.notanindex.Name X1:B\n\
                     Strip.Curve.1.NoValue\n\
                     random garbage line\n";
        assert_eq!(parse_stp(input), vec![ChannelConfig::new("X1:A", None)]);
    }

    #[test]
    fn min_max_without_a_name_yields_no_channel() {
        let input = "Strip.Curve.0.Min 0\nStrip.Curve.0.Max 10\n";
        assert!(parse_stp(input).is_empty());
    }

    #[test]
    fn invalid_and_duplicate_names_are_skipped() {
        let input = "Strip.Curve.0.Name X1:A\n\
                     Strip.Curve.0.Min 0\n\
                     Strip.Curve.0.Max 10\n\
                     Strip.Curve.1.Name X1:BAD$NAME\n\
                     Strip.Curve.2.Name X1:A\n\
                     Strip.Curve.2.Min 20\n\
                     Strip.Curve.2.Max 30\n";
        // First curve wins; the duplicate's range is not applied.
        assert_eq!(
            parse_stp(input),
            vec![ChannelConfig::new("X1:A", Range::new(0.0, 10.0))]
        );
    }

    #[test]
    fn empty_input_yields_no_channels() {
        assert!(parse_stp("").is_empty());
    }
}
