use super::ReadState;
use crate::fixed;
use crate::reader::lines::Line;
use molframe_core::bond::{BondOrder, BondProvenance, BondRecord};
use molframe_core::diagnostic::{Code, Diagnostic};
use molframe_core::element::Element;
use molframe_core::index::AtomIndex;
use molframe_core::optional::OptionalSymbol;
use molframe_core::span::{ByteSpan, Position};
use std::collections::HashMap;
use std::fmt::Write as _;
const METALS: [&str; 24] = [
"LI", "NA", "K", "MG", "CA", "MN", "FE", "CO", "NI", "CU", "ZN", "MO", "CD", "HG", "PB", "SR",
"BA", "AL", "CR", "V", "W", "PT", "AU", "AG",
];
pub(super) struct LinkRequest {
first: String,
second: String,
disulfide: bool,
at: Position,
}
pub(super) type LinkAtoms = HashMap<String, Option<(AtomIndex, Element)>>;
fn identity(out: &mut String, name: &str, alt: &str, chain: &str, seq: &str, ins: &str) {
out.clear();
let _ = write!(out, "{name}|{alt}|{chain}|{seq}|{ins}");
}
impl ReadState<'_> {
pub(super) fn ssbond(&mut self, line: &Line<'_>) {
if !is_identity(line.text) {
self.skip_image(line);
return;
}
let first = Self::link_identity(line, "SG", "", (16, 16), (18, 21), (22, 22));
let second = Self::link_identity(line, "SG", "", (30, 30), (32, 35), (36, 36));
self.request(first, second, true, line);
}
pub(super) fn link(&mut self, line: &Line<'_>) {
if !is_identity(line.text) {
self.skip_image(line);
return;
}
let first = Self::link_identity(
line,
fixed::text(line.text, 13, 16),
fixed::text(line.text, 17, 17),
(22, 22),
(23, 26),
(27, 27),
);
let second = Self::link_identity(
line,
fixed::text(line.text, 43, 46),
fixed::text(line.text, 47, 47),
(52, 52),
(53, 56),
(57, 57),
);
self.request(first, second, false, line);
}
fn skip_image(&mut self, line: &Line<'_>) {
self.findings
.push(Diagnostic::new(Code::W3013).at(ByteSpan::empty(line.at)));
}
fn link_identity(
line: &Line<'_>,
name: &str,
alt: &str,
chain: (usize, usize),
seq: (usize, usize),
ins: (usize, usize),
) -> String {
let mut key = String::new();
identity(
&mut key,
name,
alt,
fixed::text(line.text, chain.0, chain.1),
fixed::text(line.text, seq.0, seq.1),
fixed::text(line.text, ins.0, ins.1),
);
key
}
fn request(&mut self, first: String, second: String, disulfide: bool, line: &Line<'_>) {
self.link_atoms.entry(first.clone()).or_insert(None);
self.link_atoms.entry(second.clone()).or_insert(None);
self.links.push(LinkRequest {
first,
second,
disulfide,
at: line.at,
});
}
pub(super) fn note_link_atom(&mut self, line: &Line<'_>, atom: AtomIndex, element: Element) {
if self.link_atoms.is_empty() {
return;
}
let mut key = std::mem::take(&mut self.key_scratch);
let alt = fixed::text(line.text, 17, 17);
identity(
&mut key,
fixed::text(line.text, 13, 16),
alt,
fixed::text(line.text, 22, 22),
fixed::text(line.text, 23, 26),
fixed::text(line.text, 27, 27),
);
if let Some(slot) = self.link_atoms.get_mut(&key)
&& slot.is_none()
{
*slot = Some((atom, element));
}
if !alt.is_empty() {
identity(
&mut key,
fixed::text(line.text, 13, 16),
"",
fixed::text(line.text, 22, 22),
fixed::text(line.text, 23, 26),
fixed::text(line.text, 27, 27),
);
if let Some(slot) = self.link_atoms.get_mut(&key)
&& slot.is_none()
{
*slot = Some((atom, element));
}
}
self.key_scratch = key;
}
pub(super) fn link_bonds(&mut self) -> Vec<BondRecord> {
let requests = std::mem::take(&mut self.links);
let mut bonds = Vec::with_capacity(requests.len());
for request in requests {
let first = self.link_atoms.get(&request.first).copied().flatten();
let second = self.link_atoms.get(&request.second).copied().flatten();
let (Some((atom_a, element_a)), Some((atom_b, element_b))) = (first, second) else {
self.findings.push(
Diagnostic::new(Code::W3016)
.at(ByteSpan::empty(request.at))
.with_context("endpoint a", request.first)
.with_context("endpoint b", request.second),
);
continue;
};
let kind = if request.disulfide {
"disulf"
} else if is_metal(element_a) || is_metal(element_b) {
"metalc"
} else {
"covale"
};
let kind = OptionalSymbol::some(self.intern(kind));
bonds.push(BondRecord {
atom_a,
atom_b,
order: BondOrder::Unknown,
provenance: BondProvenance::File,
kind,
});
}
bonds
}
}
fn is_metal(element: Element) -> bool {
METALS.contains(&element.symbol().to_ascii_uppercase().as_str())
}
fn is_identity(line: &str) -> bool {
[(60, 65), (67, 72)]
.iter()
.all(|&(from, to)| matches!(fixed::text(line, from, to), "" | "1555"))
}