use super::lines::{Line, Lines};
use super::state::ReadState;
use crate::fixed;
use crate::header::is_metadata_record;
use molframe_core::diagnostic::{Code, Diagnostic};
use molframe_core::io::{Format, ReadOptions, ReadResult};
use molframe_core::structure::{CoordinateStore, Structure, StructureData};
pub(super) struct CommonRecords<'a> {
lines: Vec<Line<'a>>,
inside_model: bool,
}
impl<'a> CommonRecords<'a> {
pub(super) const fn new() -> Self {
Self {
lines: Vec::new(),
inside_model: false,
}
}
pub(super) fn observe(&mut self, line: &Line<'a>) {
let record = fixed::record(line.text);
match record {
"MODEL" => self.inside_model = true,
"ENDMDL" => self.inside_model = false,
"HELIX" | "SHEET" if self.inside_model => return,
_ => {}
}
if is_metadata_record(record) || record == "CONECT" {
self.lines.push(*line);
}
}
fn state<'options>(
&self,
options: &'options ReadOptions,
variant: Format,
) -> ReadState<'options> {
let mut state = ReadState::new(options, variant);
for line in &self.lines {
state.line(line);
}
state
}
}
pub(super) fn read_ragged(
text: &str,
options: &ReadOptions,
variant: Format,
common: &CommonRecords<'_>,
) -> ReadResult {
let mut models = Vec::new();
let mut numbers = Vec::new();
let mut findings = Vec::new();
let mut current = None;
let mut malformed = 0_u64;
for line in Lines::new(text) {
let line = line.map_err(|finding| vec![finding])?;
match fixed::record(line.text) {
"MODEL" => {
finish_model(&mut current, &mut models, &mut numbers, &mut findings)?;
let mut state = common.state(options, variant);
state.line(&line);
current = Some(state);
}
"ENDMDL" => {
if let Some(state) = current.as_mut() {
state.line(&line);
}
finish_model(&mut current, &mut models, &mut numbers, &mut findings)?;
}
"ATOM" | "HETATM" => {
if current.is_none() {
current = Some(common.state(options, variant));
}
if let Some(state) = current.as_mut() {
state.line(&line);
}
}
"TER" | "HELIX" | "SHEET" => {
if let Some(state) = current.as_mut() {
state.line(&line);
}
}
_ if super::state::starts_with_coordinate_keyword(line.text) => {
malformed += 1;
}
_ => {}
}
}
finish_model(&mut current, &mut models, &mut numbers, &mut findings)?;
if malformed > 0 && models.is_empty() {
findings.push(
Diagnostic::new(Code::E1201)
.with_message("coordinate records are present but their fields do not parse")
.with_context("rows", malformed.to_string()),
);
}
finish_ensemble(models, numbers, findings, options)
}
fn finish_model(
current: &mut Option<ReadState<'_>>,
models: &mut Vec<Structure>,
numbers: &mut Vec<i32>,
findings: &mut Vec<Diagnostic>,
) -> Result<(), Vec<Diagnostic>> {
let Some(state) = current.take() else {
return Ok(());
};
let (model, model_findings) = state.finish()?;
let number = model_number_or_one(
model
.data()
.models()
.next()
.and_then(molframe_core::structure::ModelRef::number),
);
models.push(model);
numbers.push(number);
findings.extend(model_findings);
Ok(())
}
fn model_number_or_one(number: Option<i32>) -> i32 {
let Some(number) = number else {
return 1;
};
number
}
fn finish_ensemble(
models: Vec<Structure>,
numbers: Vec<i32>,
mut findings: Vec<Diagnostic>,
options: &ReadOptions,
) -> ReadResult {
let mut data = StructureData::empty();
if let Some(first) = models.first() {
data.entry = first.data().entry.clone();
data.cell = first.data().cell;
data.extensions = first.data().extensions.clone();
}
for number in numbers {
if data.topology.models.push(number, 0..0).is_err() {
findings.push(Diagnostic::new(Code::E3001));
}
}
data.coords = CoordinateStore::Ragged { models };
options.finish(Structure::new(data), findings)
}
#[cfg(test)]
#[path = "ensemble_tests.rs"]
mod tests;