use crate::{
CommonArgs, load_fts, load_gof, load_prt, write_gff_output, write_gff_output_filtered
};
use anyhow::{Context, Result, bail};
use clap::Parser;
use rustc_hash::{FxHashMap, FxHashSet};
use std::{
fs::File,
io::{BufRead, BufReader},
path::PathBuf,
time::Instant,
};
#[derive(Parser, Debug)]
#[command(
about = "Extract models by feature IDs",
long_about = "This tool extracts features and their parent models by feature IDs"
)]
#[clap(group(
clap::ArgGroup::new("feature")
.required(true)
.args(&["feature_file", "feature_id"])
))]
pub struct ExtractArgs {
#[clap(flatten)]
pub common: CommonArgs,
#[arg(short = 'f', long, group = "feature")]
pub feature_id: Option<String>,
#[arg(short = 'F', long, group = "feature")]
pub feature_file: Option<PathBuf>,
}
pub fn run(args: &ExtractArgs) -> Result<()> {
let gff_path = &args.common.input;
let overall_start = Instant::now();
let verbose = args.common.verbose;
if verbose {
eprintln!("[DEBUG] Starting processing of {:?}", gff_path);
eprintln!(
"[DEBUG] Thread pool initialized with {} threads",
args.common.effective_threads()
);
}
let fts = load_fts(gff_path)?;
let prt = load_prt(gff_path)?;
let gof = load_gof(gff_path)?;
let feature_names: FxHashSet<String> = if let Some(ref file_path) = args.feature_file {
let file = File::open(file_path)
.with_context(|| format!("Cannot open feature list: {:?}", file_path))?;
let reader = BufReader::new(file);
reader.lines().try_fold(
FxHashSet::default(),
|mut set, line| -> Result<FxHashSet<String>, std::io::Error> {
let s = line?;
let s = s.trim();
if !s.is_empty() {
set.insert(s.to_owned());
}
Ok(set)
},
)?
} else if let Some(ref single_id) = args.feature_id {
[single_id.clone()].into_iter().collect()
} else {
bail!("Either --feature-id (-f) or --feature-file (-F) must be specified");
};
let (fids_set, missing) = fts.map_fnames_to_fids(
&feature_names,
args.common.effective_threads()
);
if !missing.is_empty() {
eprintln!("[WARN] {} feature IDs not found: {:?}", missing.len(), missing);
}
let fid_vec: Vec<u32> = fids_set.iter().copied().collect();
let threads = args.common.effective_threads();
let roots_vec: Vec<u32> = prt.map_fids_to_roots(&fid_vec, threads);
let mut invalid_fids: Vec<u32> = fid_vec.iter()
.zip(roots_vec.iter())
.filter_map(|(&fid, &r)| if r == u32::MAX { Some(fid) } else { None })
.collect();
invalid_fids.sort_unstable();
invalid_fids.dedup();
if !invalid_fids.is_empty() {
eprintln!(
"[WARN] {} numeric feature IDs are invalid (out-of-range child or parent), skipped: {:?}",
invalid_fids.len(), invalid_fids
);
}
let mut roots: Vec<u32> = roots_vec.iter().copied().filter(|&r| r != u32::MAX).collect();
roots.sort_unstable();
roots.dedup();
let blocks: Vec<(u32, u64, u64)> = gof.roots_to_offsets(&roots, args.common.effective_threads());
if !args.common.entire_group || args.common.types.is_some() {
let mut per_root_matches: FxHashMap<u32, FxHashSet<String>> = FxHashMap::default();
per_root_matches.reserve(roots.len());
for (i, &root) in roots_vec.iter().enumerate() {
if root == u32::MAX {
continue;
}
if let Some(id_str) = fts.ids.get(fid_vec[i] as usize) {
per_root_matches.entry(root).or_default().insert(id_str.clone());
}
}
write_gff_output_filtered(
gff_path,
&blocks,
&per_root_matches,
"ID",
&args.common.output,
args.common.types.as_deref(),
verbose,
)?;
} else {
write_gff_output(
gff_path,
&blocks,
&args.common.output,
verbose,
)?;
}
if verbose {
eprintln!("[timing] Total elapsed: {:?}", overall_start.elapsed());
}
Ok(())
}