use crate::cli::MockFireOptions;
use crate::utils::bio_io::{self, read_bed_regions, BedRecord};
use crate::utils::ma_io;
use anyhow::{Context, Result};
use molecular_annotation::MolecularAnnotations;
use rust_htslib::bam::header::HeaderRecord;
use rust_htslib::bam::record::{Cigar, CigarString};
use rust_htslib::bam::{Header, HeaderView, Record};
use std::collections::HashMap;
fn group_bed_by_name_and_chrom(
bed_records: Vec<BedRecord>,
) -> HashMap<(String, String), Vec<BedRecord>> {
let mut groups: HashMap<(String, String), Vec<BedRecord>> = HashMap::new();
for record in bed_records {
let name = record.get_name_or_default();
let key = (record.chrom.clone(), name);
groups.entry(key).or_default().push(record);
}
for intervals in groups.values_mut() {
intervals.sort_by_key(|r| r.start);
}
groups
}
fn create_header_from_bed(bed_records: &[BedRecord]) -> Header {
let mut header = Header::new();
let mut chrom_lengths: HashMap<String, i64> = HashMap::new();
for record in bed_records {
let entry = chrom_lengths.entry(record.chrom.clone()).or_insert(0);
*entry = (*entry).max(record.end);
}
let mut chroms: Vec<_> = chrom_lengths.into_iter().collect();
chroms.sort_by(|a, b| a.0.cmp(&b.0));
for (chrom, max_pos) in chroms {
let mut sq_record = HeaderRecord::new(b"SQ");
sq_record.push_tag(b"SN", &chrom);
let len_str = (max_pos + 10000).to_string();
sq_record.push_tag(b"LN", &len_str);
header.push_record(&sq_record);
}
header
}
fn create_mock_fire_record(
read_name: &str,
intervals: &[BedRecord],
header_view: &HeaderView,
quality: u8,
read_length: Option<i64>,
) -> Result<Record> {
if intervals.is_empty() {
return Err(anyhow::anyhow!("No intervals for read {}", read_name));
}
let chrom = &intervals[0].chrom;
for interval in intervals {
if &interval.chrom != chrom {
return Err(anyhow::anyhow!(
"Read {} has intervals on multiple chromosomes ({} and {}). All FIRE elements for a read must be on the same chromosome.",
read_name,
chrom,
interval.chrom
));
}
}
let tid = header_view
.tid(chrom.as_bytes())
.with_context(|| format!("Chromosome '{}' not found in header", chrom))?;
let read_start = intervals.iter().map(|i| i.start).min().unwrap();
let read_end = intervals.iter().map(|i| i.end).max().unwrap();
let seq_len = read_length.unwrap_or(read_end - read_start) as usize;
let seq = vec![b'N'; seq_len];
let qual = vec![255u8; seq_len];
let cigar = CigarString(vec![Cigar::Equal(seq_len as u32)]);
let mut record = Record::new();
record.set(read_name.as_bytes(), Some(&cigar), &seq, &qual);
record.set_tid(tid as i32);
record.set_pos(read_start);
record.set_mapq(60);
record.unset_paired();
record.set_mtid(-1);
record.set_mpos(-1);
let mut starts: Vec<u32> = Vec::new();
let mut lengths: Vec<u32> = Vec::new();
let mut quals: Vec<u8> = Vec::new();
for interval in intervals {
let rel_start = (interval.start - read_start) as u32;
let length = (interval.end - interval.start) as u32;
starts.push(rel_start);
lengths.push(length);
quals.push(quality);
}
let mut annot = MolecularAnnotations::from_record(&record);
ma_io::add_fire_annotations(&mut annot, &starts, &lengths, &quals);
ma_io::write_record_with_basemods(&mut record, &annot);
Ok(record)
}
pub fn run_mock_fire(opts: &MockFireOptions) -> Result<()> {
log::info!("Reading BED file: {}", opts.bed);
let bed_records = read_bed_regions(&opts.bed).context("Failed to read BED file")?;
if bed_records.is_empty() {
return Err(anyhow::anyhow!("BED file is empty"));
}
log::info!("Read {} intervals from BED file", bed_records.len());
let grouped = group_bed_by_name_and_chrom(bed_records.clone());
log::info!(
"Grouped into {} mock reads based on chromosome and 4th column",
grouped.len()
);
let header = create_header_from_bed(&bed_records);
let header_view = HeaderView::from_header(&header);
let mut writer = bio_io::program_bam_writer_from_header(
&opts.out,
header,
"fibertools-rs",
"ft",
crate::VERSION,
);
writer
.set_threads(opts.global.threads)
.context("Failed to set threads for BAM writer")?;
if opts.uncompressed {
writer
.set_compression_level(rust_htslib::bam::CompressionLevel::Uncompressed)
.context("Failed to set uncompressed BAM")?;
}
let mut keys: Vec<_> = grouped.keys().collect();
keys.sort();
for key in keys {
let (_chrom, read_name) = key;
let intervals = &grouped[key];
let record = create_mock_fire_record(
read_name,
intervals,
&header_view,
opts.quality,
opts.read_length,
)?;
bio_io::write_record(&mut writer, &record)?;
}
log::info!("Mock BAM written to: {}", opts.out);
Ok(())
}