use std::fmt::Write as _;
use omgkit_core::ChiralTag;
use super::{AtomExpr, AtomPrim, BondExpr, BondPrim, QueryMol, Reaction};
#[must_use]
pub fn write(q: &QueryMol) -> String {
let mut out = String::new();
write_into(&mut out, q);
out
}
#[must_use]
pub fn write_reaction(r: &Reaction) -> String {
let join = |v: &[QueryMol]| v.iter().map(write).collect::<Vec<_>>().join(".");
format!(
"{}>{}>{}",
join(&r.reactants),
join(&r.agents),
join(&r.products)
)
}
fn write_into(out: &mut String, q: &QueryMol) {
let mol = &q.topology;
let n = mol.num_atoms();
if n == 0 {
return;
}
let mut visited = vec![false; n];
let mut edge_used = vec![false; mol.num_bonds()];
let mut children: Vec<Vec<(u32, u32)>> = vec![Vec::new(); n];
let mut closures: Vec<Vec<u32>> = vec![Vec::new(); n];
let mut roots: Vec<u32> = Vec::new();
for root in 0..n as u32 {
if visited[root as usize] {
continue;
}
visited[root as usize] = true;
roots.push(root);
let mut stack = vec![root];
while let Some(a) = stack.pop() {
for (other, bond) in mol.neighbors(a) {
if edge_used[bond as usize] {
continue;
}
edge_used[bond as usize] = true;
if visited[other as usize] {
closures[a as usize].push(bond);
closures[other as usize].push(bond);
} else {
visited[other as usize] = true;
children[a as usize].push((other, bond));
stack.push(other);
}
}
}
}
let mut open: Vec<Option<u32>> = vec![None; mol.num_bonds()];
let mut next_label = 1u32;
for (i, &root) in roots.iter().enumerate() {
if i > 0 {
out.push('.');
}
emit(
out,
q,
root,
None,
&children,
&closures,
&mut open,
&mut next_label,
);
}
}
#[allow(clippy::too_many_arguments)]
fn emit(
out: &mut String,
q: &QueryMol,
atom: u32,
via: Option<u32>,
children: &[Vec<(u32, u32)>],
closures: &[Vec<u32>],
open: &mut [Option<u32>],
next_label: &mut u32,
) {
if let Some(bond) = via {
out.push_str(&bond_expr_string(&q.bonds[bond as usize]));
}
out.push('[');
out.push_str(&atom_expr_string(&for_writing(
q, atom, via, children, closures,
)));
out.push(']');
for &bond in &closures[atom as usize] {
match open[bond as usize] {
Some(label) => {
out.push_str(&label_string(label));
open[bond as usize] = None;
}
None => {
let label = *next_label;
*next_label += 1;
open[bond as usize] = Some(label);
out.push_str(&bond_expr_string(&q.bonds[bond as usize]));
out.push_str(&label_string(label));
}
}
}
let kids = &children[atom as usize];
for (i, &(other, bond)) in kids.iter().enumerate() {
let last = i + 1 == kids.len();
if !last {
out.push('(');
}
emit(
out,
q,
other,
Some(bond),
children,
closures,
open,
next_label,
);
if !last {
out.push(')');
}
}
}
fn for_writing(
q: &QueryMol,
atom: u32,
via: Option<u32>,
children: &[Vec<(u32, u32)>],
closures: &[Vec<u32>],
) -> AtomExpr {
let expr = &q.atoms[atom as usize];
let Some(tag) = super::required_chirality(expr) else {
return expr.clone();
};
if !tag.is_tetrahedral() {
return expr.clone();
}
let mut written: Vec<u32> = Vec::new();
if let Some(b) = via {
written.push(b);
}
written.extend(closures[atom as usize].iter().copied());
written.extend(children[atom as usize].iter().map(|&(_, b)| b));
let stored: Vec<u32> = q
.topology
.bonds()
.iter()
.enumerate()
.filter(|(_, b)| b.other_end(atom).is_some())
.map(|(i, _)| u32::try_from(i).unwrap_or(u32::MAX))
.collect();
if written.len() != stored.len() {
return expr.clone();
}
let mut odd = super::mol::permutation_is_odd(&stored, &written);
if stored.len() == 3
&& super::mol::needs_h_compensation(
via.is_none(),
expr,
closures[atom as usize].len(),
super::mol::has_unsaturated_bond(&q.topology, &q.bonds, atom),
)
{
odd = !odd;
}
let mut out = expr.clone();
if odd {
super::mol::invert_chirality(&mut out);
}
out
}
fn label_string(label: u32) -> String {
if label < 10 {
label.to_string()
} else {
format!("%{label:02}")
}
}
#[must_use]
pub fn atom_expr_string(e: &AtomExpr) -> String {
match e {
AtomExpr::Prim(p) => prim_string(p),
AtomExpr::Not(inner) => format!("!{}", atom_expr_string(inner)),
AtomExpr::Or(parts) => parts
.iter()
.map(atom_expr_string)
.collect::<Vec<_>>()
.join(","),
AtomExpr::And(parts) => {
let sep = if parts.iter().any(|p| matches!(p, AtomExpr::Or(_))) {
";"
} else {
"&"
};
parts
.iter()
.map(atom_expr_string)
.collect::<Vec<_>>()
.join(sep)
}
}
}
fn prim_string(p: &AtomPrim) -> String {
match p {
AtomPrim::Any => "*".into(),
AtomPrim::Aromatic => "a".into(),
AtomPrim::Aliphatic => "A".into(),
AtomPrim::Element { z, aromatic } => match aromatic {
None => format!("#{z}"),
Some(arom) => {
let sym = omgkit_core::element::by_atomic_num(*z).map_or("*", |e| e.symbol);
if *arom {
sym.to_ascii_lowercase()
} else {
sym.to_string()
}
}
},
AtomPrim::Degree(n) => format!("D{n}"),
AtomPrim::TotalDegree(n) => format!("X{n}"),
AtomPrim::TotalHs(n) => format!("H{n}"),
AtomPrim::ImplicitHs(n) => format!("h{n}"),
AtomPrim::RingCount(n) => opt_num("R", *n),
AtomPrim::RingSize(n) => opt_num("r", *n),
AtomPrim::RingBondCount(n) => opt_num("x", *n),
AtomPrim::Valence(n) => format!("v{n}"),
AtomPrim::Charge(c) => charge_string(*c),
AtomPrim::Isotope(m) => m.to_string(),
AtomPrim::AtomMap(m) => format!(":{m}"),
AtomPrim::Chirality(t) => chirality_string(*t),
AtomPrim::Recursive(q) => format!("$({})", write(q)),
}
}
fn opt_num(sym: &str, n: Option<u32>) -> String {
match n {
Some(v) => format!("{sym}{v}"),
None => sym.to_string(),
}
}
fn charge_string(c: i32) -> String {
let mut s = String::new();
let sign = if c < 0 { '-' } else { '+' };
let mag = c.unsigned_abs();
let _ = write!(s, "{sign}{mag}");
s
}
fn chirality_string(t: ChiralTag) -> String {
match t {
ChiralTag::Ccw => "@".into(),
ChiralTag::Cw => "@@".into(),
_ => "@".into(),
}
}
#[must_use]
pub fn bond_expr_string(e: &BondExpr) -> String {
if *e == BondExpr::default_bond() {
return String::new();
}
match e {
BondExpr::Prim(p) => bond_prim_string(*p),
BondExpr::Not(inner) => format!("!{}", bond_expr_string(inner)),
BondExpr::Or(parts) => parts
.iter()
.map(bond_expr_string)
.collect::<Vec<_>>()
.join(","),
BondExpr::And(parts) => {
let sep = if parts.iter().any(|p| matches!(p, BondExpr::Or(_))) {
";"
} else {
"&"
};
parts
.iter()
.map(bond_expr_string)
.collect::<Vec<_>>()
.join(sep)
}
}
}
fn bond_prim_string(p: BondPrim) -> String {
match p {
BondPrim::Any => "~",
BondPrim::Single => "-",
BondPrim::Double => "=",
BondPrim::Triple => "#",
BondPrim::Quadruple => "$",
BondPrim::Aromatic => ":",
BondPrim::InRing => "@",
BondPrim::UpRight => "/",
BondPrim::DownRight => "\\",
BondPrim::Dative => "->",
BondPrim::DativeReversed => "<-",
}
.to_string()
}