md_analysis 0.1.0

molecular dynamics
use std::fs::{self, File};
use std::io::Write;
/// FinalResult 字段 → CSV 文件输出
use std::path::Path;

use crate::binning::{FinalResult, Histogram, PerBaseHistogram};

// ── 单个 Histogram → CSV (bin_idx,dw,ar) ────────────────────

fn write_histogram_csv<const N: usize>(
    out_dir: impl AsRef<Path>,
    name: &str,
    hist: &Histogram<N>,
) -> anyhow::Result<()> {
    let path = out_dir.as_ref().join(format!("{name}.csv"));
    let mut f = File::create(path)?;
    writeln!(f, "bin_idx,dw,ar")?;
    for i in 0..N {
        writeln!(f, "{},{},{}", i, hist.dw[i], hist.ar[i])?;
    }
    Ok(())
}

// ── 单个 PerBaseHistogram → CSV (base,bin_idx,dw,ar) ─────────

fn write_per_base_csv<const N: usize>(
    out_dir: &Path,
    name: &str,
    pb: &PerBaseHistogram<N>,
) -> anyhow::Result<()> {
    let path = out_dir.join(format!("{name}.csv"));
    let mut f = File::create(path)?;
    writeln!(f, "base,bin_idx,dw,ar")?;
    for base in "ACGT".chars() {
        let hist = pb.hist_grams.get(&base).unwrap();
        for i in 0..N {
            writeln!(f, "{},{},{},{}", base, i, hist.dw[i], hist.ar[i])?;
        }
    }
    Ok(())
}

// ── FinalResult → CSV (遍历每个 Some 字段,None 跳过) ─────────

/// 将 FinalResult 的每个 Some 字段写为独立 CSV 文件。
pub fn write_final_result_csv<const N: usize>(
    out_dir: &Path,
    result: &FinalResult<N>,
) -> anyhow::Result<()> {
    fs::create_dir_all(out_dir)?;

    if let Some(hist) = &result.total_hist {
        write_histogram_csv(out_dir, "total_hist", hist)?;
    }
    if let Some(pb) = &result.total_per_base_hist {
        write_per_base_csv(out_dir, "total_per_base_hist", pb)?;
    }
    if let Some(hist) = &result.first_n_total_hist {
        write_histogram_csv(out_dir, "first_n_total_hist", hist)?;
    }
    if let Some(pb) = &result.first_n_per_base_hist {
        write_per_base_csv(out_dir, "first_n_per_base_hist", pb)?;
    }
    if let Some(hist) = &result.last_n_total_hist {
        write_histogram_csv(out_dir, "last_n_total_hist", hist)?;
    }
    if let Some(pb) = &result.last_n_per_base_hist {
        write_per_base_csv(out_dir, "last_n_per_base_hist", pb)?;
    }

    Ok(())
}

// ── tests ────────────────────────────────────────────────────────

#[cfg(test)]
mod tests {
    use super::*;
    use crate::binning::{FinalResult, Histogram, PerBaseHistogram};

    fn setup_temp(name: &str) -> std::path::PathBuf {
        let dir = std::env::temp_dir().join(format!("md_analysis_test_writer/{name}"));
        let _ = fs::remove_dir_all(&dir);
        fs::create_dir_all(&dir).unwrap();
        dir
    }

    #[test]
    fn writes_some_fields_skips_none() {
        let out = setup_temp("some_none");
        let r: FinalResult<5> = FinalResult::new_first_n_last_n(); // total = None
        write_final_result_csv(&out, &r).unwrap();

        assert!(out.join("first_n_total_hist.csv").exists());
        assert!(out.join("first_n_per_base_hist.csv").exists());
        assert!(out.join("last_n_total_hist.csv").exists());
        assert!(out.join("last_n_per_base_hist.csv").exists());
        assert!(!out.join("total_hist.csv").exists());
    }

    #[test]
    fn histogram_csv_format() {
        let out = setup_temp("histogram_format");
        let mut h: Histogram<5> = Histogram::new();
        h.dw[1] = 42;
        h.ar[3] = 7;
        write_histogram_csv(&out, "test_hist", &h).unwrap();

        let content = fs::read_to_string(out.join("test_hist.csv")).unwrap();
        assert!(content.starts_with("bin_idx,dw,ar\n"));
        assert!(content.contains("1,42,0"));
    }

    #[test]
    fn per_base_csv_format() {
        let out = setup_temp("per_base_format");
        let mut pb: PerBaseHistogram<5> = PerBaseHistogram::new();
        for base in "ACGT".chars() {
            pb.hist_grams.get_mut(&base).unwrap().dw[2] = 7;
        }
        write_per_base_csv(&out, "test_pb", &pb).unwrap();

        let content = fs::read_to_string(out.join("test_pb.csv")).unwrap();
        assert!(content.starts_with("base,bin_idx,dw,ar\n"));
        assert!(content.contains("A,2,7,0"));
        assert!(content.contains("T,2,7,0"));
    }

    #[test]
    fn skips_none_fields() {
        let out = setup_temp("skips_none");
        let r: FinalResult<5> = FinalResult::new_all(); // first_n/last_n = None
        write_final_result_csv(&out, &r).unwrap();

        assert!(out.join("total_hist.csv").exists());
        assert!(!out.join("first_n_total_hist.csv").exists());
    }
}