//! Formula representation and conversion helpers.
//!
//! The crate has two main formula representations:
//! 1. The internal `Formula` type (`[i32; NUM_ELEMENTS]`) used by the high-performance
//!    decomposition routines.
//! 2. The user-facing `Composition` type (a `HashMap<Element, u32>`) used in the public API.

use crate::element::{ATOMIC_MASSES, ELEMENT_SYMBOLS, NUM_ELEMENTS};
use std::collections::HashMap;

pub type Element = String;

/// Count of each element in a formula.
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Composition {
    pub counts: HashMap<Element, u32>,
}

impl Composition {
    pub fn new() -> Self {
        Self::default()
    }

    pub fn from_pairs(pairs: &[(String, u32)]) -> Self {
        let mut counts = HashMap::new();
        for (k, v) in pairs {
            counts.insert(k.clone(), *v);
        }
        Self { counts }
    }
}

/// Internal fixed-size formula representation used by the decomposer.
pub(crate) type Formula = [i32; NUM_ELEMENTS];

/// Convert a user-facing `Composition` to the internal `Formula` representation.
#[allow(dead_code)]
pub(crate) fn composition_to_formula(
    comp: &Composition,
) -> Result<Formula, crate::error::DecompositionError> {
    let mut f = [0i32; NUM_ELEMENTS];
    for (elem, count) in &comp.counts {
        let count = *count as i32;
        let idx = ELEMENT_SYMBOLS
            .iter()
            .position(|&s| s == elem)
            .ok_or_else(|| {
                crate::error::DecompositionError::InvalidInput(format!(
                    "unknown element symbol '{}'",
                    elem
                ))
            })?;
        f[idx] = count;
    }
    Ok(f)
}

/// Convert the internal `Formula` representation to a user-facing `Composition`.
pub(crate) fn formula_to_composition(f: &Formula) -> Composition {
    let mut counts = HashMap::new();
    for (i, &count) in f.iter().enumerate() {
        if count > 0 {
            counts.insert(ELEMENT_SYMBOLS[i].to_string(), count as u32);
        }
    }
    Composition { counts }
}

/// Compute the exact monoisotopic mass of a formula.
pub(crate) fn formula_mass(f: &Formula) -> f64 {
    f.iter()
        .enumerate()
        .map(|(idx, &count)| ATOMIC_MASSES[idx] * count as f64)
        .sum()
}

/// Compute the double-bond equivalent (DBE) of a formula.
pub(crate) fn formula_dbe(f: &Formula) -> f64 {
    let c_count = f[1];
    let n_count = f[2];
    let p_count = f[6];
    let h_count = f[0];
    let f_count = f[4];
    let cl_count = f[8];
    let br_count = f[10];
    let i_count = f[11];
    let na_count = f[5];
    let k_count = f[9];

    (c_count as f64) + ((n_count + p_count) as f64 / 2.0)
        - ((h_count + f_count + cl_count + br_count + i_count + na_count + k_count) as f64 / 2.0)
        + 1.0
}

/// Compute the nominal (integer) mass of a formula.
pub(crate) fn formula_nominal_mass(f: &Formula) -> u32 {
    let nominal_masses: [u32; NUM_ELEMENTS] = [1, 12, 14, 16, 19, 23, 31, 32, 35, 39, 79, 127];
    f.iter()
        .enumerate()
        .map(|(idx, &count)| nominal_masses[idx] * count as u32)
        .sum()
}

/// Compute the mass defect (exact - nominal) of a formula.
pub(crate) fn formula_mass_defect(f: &Formula) -> f64 {
    formula_mass(f) - formula_nominal_mass(f) as f64
}

/// Convert a formula into a Hill-system string like "C6H12O6".
pub fn formula_to_string(f: &Formula) -> String {
    let mut s = String::new();
    for (i, &count) in f.iter().enumerate() {
        if count > 0 {
            s.push_str(ELEMENT_SYMBOLS[i]);
            if count > 1 {
                s.push_str(&count.to_string());
            }
        }
    }
    s
}

/// Check whether a formula obeys a simple set of valence rules.
///
/// These are intentionally permissive: this is a *plausibility* check, not a full
/// SMILES-style valence analyzer. The supported element set is
/// H, C, N, O, F, Na, P, S, Cl, K, Br, I. Rules:
///
/// - **H, F, Cl, Br, I, Na, K** are monovalent. At most one bond each.
/// - **O** is at most divalent (max 2 bonds per atom).
/// - **C** is at most tetravalent (max 4 bonds per atom).
/// - **N** is at most pentavalent (3 by default; 5 for nitro/ammonium cases).
/// - **P** is at most pentavalent.
/// - **S** is at most hexavalent (sulfates).
///
/// The check verifies two necessary conditions for a closed-shell, neutral
/// molecular structure:
///
/// 1. The number of monovalent atoms does not exceed the bond-end capacity
///    contributed by the poly-valent atoms. Otherwise the molecule would have
///    dangling valences (e.g. `H100` is invalid, `CH4` is valid).
/// 2. The total number of bond ends is even, so the structure can close into
///    a valid Lewis structure without unpaired electrons.
///
/// These are necessary but not sufficient conditions. The point is to filter
/// out obvious chemistry violations in the decomposer's output, not to certify
/// every formula is a "natural" molecule.
pub fn is_valid_valence(f: &Formula) -> bool {
    // Element indices: H=0, C=1, N=2, O=3, F=4, Na=5, P=6, S=7, Cl=8, K=9, Br=10, I=11
    if f.iter().any(|&c| c < 0) {
        return false;
    }
    let h = f[0];
    let c = f[1];
    let n = f[2];
    let o = f[3];
    let f_count = f[4];
    let na = f[5];
    let p = f[6];
    let s = f[7];
    let cl = f[8];
    let k = f[9];
    let br = f[10];
    let i_count = f[11];

    // Total monovalent atoms (each contributes 1 bond end).
    let mono = h + f_count + na + cl + k + br + i_count;
    // Total bond-end capacity of poly-valent atoms.
    let poly_capacity = 4 * c + 5 * n + 2 * o + 5 * p + 6 * s;

    // Monovalent atoms must not exceed poly-valent capacity.
    if mono > poly_capacity {
        return false;
    }

    // Total bond ends must be even (so the structure can close).
    let total = mono + poly_capacity;
    if total % 2 != 0 {
        return false;
    }

    true
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_formula_mass_known() {
        // H2O = 2*1.007825 + 15.994915 = 18.010565
        let water = [2, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0];
        let mass = formula_mass(&water);
        assert!((mass - 18.010565).abs() < 1e-4, "got {mass}");
    }

    #[test]
    fn test_formula_to_string() {
        let caffeine = [10, 8, 4, 2, 0, 0, 0, 0, 0, 0, 0, 0];
        let s = formula_to_string(&caffeine);
        // Order: H, C, N, O, ... so this should produce "H10C8N4O2".
        assert_eq!(s, "H10C8N4O2");
    }

    #[test]
    fn test_is_valid_valence_basic() {
        // Water: H2O. Valid.
        assert!(is_valid_valence(&[2, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0]));
        // Methane: CH4. Valid.
        assert!(is_valid_valence(&[4, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]));
        // Sulfate: SO4. Valid (S=6, 4 O=2 each).
        assert!(is_valid_valence(&[0, 0, 0, 4, 0, 0, 0, 1, 0, 0, 0, 0]));
        // H100 alone: invalid (no poly-valent atoms to bond to).
        let mut h100 = [0i32; NUM_ELEMENTS];
        h100[0] = 100;
        assert!(!is_valid_valence(&h100));
        // CH (methyl radical): invalid (odd bond count).
        assert!(!is_valid_valence(&[1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]));
        // Glucose C6H12O6: valid.
        assert!(is_valid_valence(&[12, 6, 0, 6, 0, 0, 0, 0, 0, 0, 0, 0]));
    }
}
