/// Output per-molecule RMSD for first N molecules
use sci_form::conformer::generate_3d_conformer_with_torsions;
use sci_form::graph::Molecule;
use serde::Deserialize;

#[derive(Deserialize)]
struct RefAtom {
    element: u8,
    x: f32,
    y: f32,
    z: f32,
    formal_charge: i8,
    hybridization: String,
}

#[derive(Deserialize)]
struct RefBond {
    start: usize,
    end: usize,
    order: String,
}

#[derive(Deserialize)]
struct RefTorsion {
    atoms: Vec<usize>,
    v: Vec<f64>,
    signs: Vec<i32>,
}

#[derive(Deserialize)]
#[allow(dead_code)]
struct RefMolecule {
    smiles: String,
    atoms: Vec<RefAtom>,
    bonds: Vec<RefBond>,
    torsions: Vec<RefTorsion>,
}

fn main() {
    let limit: usize = std::env::args()
        .nth(1)
        .and_then(|s| s.parse().ok())
        .unwrap_or(1000);

    let data: Vec<RefMolecule> = serde_json::from_str(
        &sci_form::fixture_io::read_text_fixture("tests/fixtures/gdb20_reference_1k.json").unwrap(),
    )
    .unwrap();

    let mut results = Vec::new();
    for (_i, entry) in data.iter().enumerate().take(limit.min(data.len())) {
        let mol = build_mol_from_ref(entry);
        let csd_torsions = build_csd_torsions(&entry.torsions);
        let result = generate_3d_conformer_with_torsions(&mol, 42, &csd_torsions);

        let rmsd = match result {
            Ok(coords) => pairwise_rmsd(&coords, &entry.atoms) as f64,
            Err(_) => -1.0,
        };
        results.push(rmsd);
    }

    let json = serde_json::to_string(&results).unwrap();
    println!("{}", json);
}

fn build_mol_from_ref(r: &RefMolecule) -> Molecule {
    use sci_form::graph::{Atom, Bond, BondOrder, BondStereo, Hybridization};
    let mut mol = Molecule::new("");
    for a in &r.atoms {
        let hyb = match a.hybridization.as_str() {
            "SP" => Hybridization::SP,
            "SP2" => Hybridization::SP2,
            "SP3" => Hybridization::SP3,
            "SP3D" => Hybridization::SP3D,
            "SP3D2" => Hybridization::SP3D2,
            _ => Hybridization::Unknown,
        };
        let mut atom = Atom::new(a.element, 0.0, 0.0, 0.0);
        atom.hybridization = hyb;
        atom.formal_charge = a.formal_charge;
        mol.add_atom(atom);
    }
    for b in &r.bonds {
        let order = match b.order.as_str() {
            "SINGLE" => BondOrder::Single,
            "DOUBLE" => BondOrder::Double,
            "TRIPLE" => BondOrder::Triple,
            "AROMATIC" => BondOrder::Aromatic,
            _ => BondOrder::Single,
        };
        mol.add_bond(
            petgraph::graph::NodeIndex::new(b.start),
            petgraph::graph::NodeIndex::new(b.end),
            Bond {
                order,
                stereo: BondStereo::None,
            },
        );
    }
    mol
}

fn build_csd_torsions(
    torsions: &[RefTorsion],
) -> Vec<sci_form::forcefield::etkdg_3d::M6TorsionContrib> {
    torsions
        .iter()
        .map(|t| sci_form::forcefield::etkdg_3d::M6TorsionContrib {
            i: t.atoms[0],
            j: t.atoms[1],
            k: t.atoms[2],
            l: t.atoms[3],
            signs: [
                t.signs[0] as f64,
                t.signs[1] as f64,
                t.signs[2] as f64,
                t.signs[3] as f64,
                t.signs[4] as f64,
                t.signs[5] as f64,
            ],
            v: [t.v[0], t.v[1], t.v[2], t.v[3], t.v[4], t.v[5]],
        })
        .collect()
}

fn pairwise_rmsd(coords: &nalgebra::DMatrix<f32>, ref_atoms: &[RefAtom]) -> f32 {
    let n = ref_atoms.len();
    let mut sq_sum = 0.0f64;
    let mut npairs = 0u64;
    for a in 0..n {
        for b in (a + 1)..n {
            let dr = ((ref_atoms[a].x - ref_atoms[b].x).powi(2)
                + (ref_atoms[a].y - ref_atoms[b].y).powi(2)
                + (ref_atoms[a].z - ref_atoms[b].z).powi(2))
            .sqrt() as f64;
            let du = ((coords[(a, 0)] - coords[(b, 0)]).powi(2)
                + (coords[(a, 1)] - coords[(b, 1)]).powi(2)
                + (coords[(a, 2)] - coords[(b, 2)]).powi(2))
            .sqrt() as f64;
            sq_sum += (dr - du).powi(2);
            npairs += 1;
        }
    }
    if npairs > 0 {
        (sq_sum / npairs as f64).sqrt() as f32
    } else {
        0.0
    }
}
