//! Fundamental chemical primitives: elements and bond orders.
//!
//! This module provides the building blocks for representing chemical identity across
//! `dreid-forge`. The [`Element`] enum covers the entire periodic table with atomic
//! numbers, masses, and symbols, while [`BondOrder`] classifies covalent connectivity.
//! Both types support string parsing for file I/O and implement standard traits for
//! use in hash maps and sorted collections.

use std::fmt;
use std::str::FromStr;
use thiserror::Error;

/// Error returned when parsing an invalid or unsupported element symbol.
///
/// This error occurs when [`Element::from_str`] encounters a string that does not
/// match any known element symbol. The wrapped `String` contains the original input.
#[derive(Debug, Clone, PartialEq, Eq, Error)]
#[error("invalid or unsupported element symbol: '{0}'")]
pub struct ParseElementError(String);

/// Error returned when parsing an invalid bond order string.
///
/// This error occurs when [`BondOrder::from_str`] cannot interpret the input.
/// Accepted values include `"single"`, `"double"`, `"triple"`, `"aromatic"`,
/// or their numeric equivalents `"1"`, `"2"`, `"3"`, `"ar"`.
#[derive(Debug, Clone, PartialEq, Eq, Error)]
#[error("invalid bond order string: '{0}'")]
pub struct ParseBondOrderError(String);

/// Chemical element from the periodic table (Z = 1 through 118).
///
/// Each variant stores its atomic number in the `repr(u8)` discriminant, enabling
/// efficient storage and fast comparisons. The type provides accessors for atomic
/// mass, symbol, and atomic number, and implements [`FromStr`] for parsing element
/// symbols from molecular file formats.
///
/// # Examples
///
/// ```
/// use dreid_forge::Element;
/// use std::str::FromStr;
///
/// let carbon = Element::C;
/// assert_eq!(carbon.symbol(), "C");
/// assert_eq!(carbon.atomic_number(), 6);
/// assert!((carbon.atomic_mass() - 12.011).abs() < 1e-3);
///
/// let parsed = Element::from_str("Fe").unwrap();
/// assert_eq!(parsed, Element::Fe);
/// ```
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
#[repr(u8)]
pub enum Element {
    H = 1,
    He,
    Li,
    Be,
    B,
    C,
    N,
    O,
    F,
    Ne,
    Na,
    Mg,
    Al,
    Si,
    P,
    S,
    Cl,
    Ar,
    K,
    Ca,
    Sc,
    Ti,
    V,
    Cr,
    Mn,
    Fe,
    Co,
    Ni,
    Cu,
    Zn,
    Ga,
    Ge,
    As,
    Se,
    Br,
    Kr,
    Rb,
    Sr,
    Y,
    Zr,
    Nb,
    Mo,
    Tc,
    Ru,
    Rh,
    Pd,
    Ag,
    Cd,
    In,
    Sn,
    Sb,
    Te,
    I,
    Xe,
    Cs,
    Ba,
    La,
    Ce,
    Pr,
    Nd,
    Pm,
    Sm,
    Eu,
    Gd,
    Tb,
    Dy,
    Ho,
    Er,
    Tm,
    Yb,
    Lu,
    Hf,
    Ta,
    W,
    Re,
    Os,
    Ir,
    Pt,
    Au,
    Hg,
    Tl,
    Pb,
    Bi,
    Po,
    At,
    Rn,
    Fr,
    Ra,
    Ac,
    Th,
    Pa,
    U,
    Np,
    Pu,
    Am,
    Cm,
    Bk,
    Cf,
    Es,
    Fm,
    Md,
    No,
    Lr,
    Rf,
    Db,
    Sg,
    Bh,
    Hs,
    Mt,
    Ds,
    Rg,
    Cn,
    Nh,
    Fl,
    Mc,
    Lv,
    Ts,
    Og = 118,
}

impl Element {
    /// Returns the standard atomic mass in unified atomic mass units (Da).
    ///
    /// Values are taken from IUPAC recommendations and are suitable for computing
    /// molecular masses in force field parameterization and simulation setup.
    ///
    /// # Returns
    ///
    /// Atomic mass as `f64` in Daltons.
    pub fn atomic_mass(&self) -> f64 {
        match self {
            Element::H => 1.008,
            Element::He => 4.0026,
            Element::Li => 6.94,
            Element::Be => 9.0122,
            Element::B => 10.81,
            Element::C => 12.011,
            Element::N => 14.007,
            Element::O => 15.999,
            Element::F => 18.998,
            Element::Ne => 20.18,
            Element::Na => 22.99,
            Element::Mg => 24.305,
            Element::Al => 26.982,
            Element::Si => 28.085,
            Element::P => 30.974,
            Element::S => 32.06,
            Element::Cl => 35.45,
            Element::Ar => 39.948,
            Element::K => 39.098,
            Element::Ca => 40.078,
            Element::Sc => 44.956,
            Element::Ti => 47.867,
            Element::V => 50.942,
            Element::Cr => 51.996,
            Element::Mn => 54.938,
            Element::Fe => 55.845,
            Element::Co => 58.933,
            Element::Ni => 58.693,
            Element::Cu => 63.546,
            Element::Zn => 65.38,
            Element::Ga => 69.723,
            Element::Ge => 72.63,
            Element::As => 74.922,
            Element::Se => 78.971,
            Element::Br => 79.904,
            Element::Kr => 83.798,
            Element::Rb => 85.468,
            Element::Sr => 87.62,
            Element::Y => 88.906,
            Element::Zr => 91.224,
            Element::Nb => 92.906,
            Element::Mo => 95.96,
            Element::Tc => 98.0,
            Element::Ru => 101.07,
            Element::Rh => 102.91,
            Element::Pd => 106.42,
            Element::Ag => 107.87,
            Element::Cd => 112.41,
            Element::In => 114.82,
            Element::Sn => 118.71,
            Element::Sb => 121.76,
            Element::Te => 127.6,
            Element::I => 126.9,
            Element::Xe => 131.29,
            Element::Cs => 132.91,
            Element::Ba => 137.33,
            Element::La => 138.91,
            Element::Ce => 140.12,
            Element::Pr => 140.91,
            Element::Nd => 144.24,
            Element::Pm => 145.0,
            Element::Sm => 150.36,
            Element::Eu => 151.96,
            Element::Gd => 157.25,
            Element::Tb => 158.93,
            Element::Dy => 162.5,
            Element::Ho => 164.93,
            Element::Er => 167.26,
            Element::Tm => 168.93,
            Element::Yb => 173.05,
            Element::Lu => 174.97,
            Element::Hf => 178.49,
            Element::Ta => 180.95,
            Element::W => 183.84,
            Element::Re => 186.21,
            Element::Os => 190.23,
            Element::Ir => 192.22,
            Element::Pt => 195.08,
            Element::Au => 196.97,
            Element::Hg => 200.59,
            Element::Tl => 204.38,
            Element::Pb => 207.2,
            Element::Bi => 208.98,
            Element::Po => 209.0,
            Element::At => 210.0,
            Element::Rn => 222.0,
            Element::Fr => 223.0,
            Element::Ra => 226.0,
            Element::Ac => 227.0,
            Element::Th => 232.04,
            Element::Pa => 231.04,
            Element::U => 238.03,
            Element::Np => 237.0,
            Element::Pu => 244.0,
            Element::Am => 243.0,
            Element::Cm => 247.0,
            Element::Bk => 247.0,
            Element::Cf => 251.0,
            Element::Es => 252.0,
            Element::Fm => 257.0,
            Element::Md => 258.0,
            Element::No => 259.0,
            Element::Lr => 262.0,
            Element::Rf => 267.0,
            Element::Db => 270.0,
            Element::Sg => 271.0,
            Element::Bh => 270.0,
            Element::Hs => 277.0,
            Element::Mt => 276.0,
            Element::Ds => 281.0,
            Element::Rg => 280.0,
            Element::Cn => 285.0,
            Element::Nh => 284.0,
            Element::Fl => 289.0,
            Element::Mc => 288.0,
            Element::Lv => 293.0,
            Element::Ts => 294.0,
            Element::Og => 294.0,
        }
    }

    /// Returns the atomic number (Z) of this element.
    ///
    /// The value ranges from 1 (hydrogen) to 118 (oganesson) and corresponds
    /// to the number of protons in the nucleus.
    ///
    /// # Returns
    ///
    /// Atomic number as `u8`.
    #[inline]
    pub fn atomic_number(&self) -> u8 {
        *self as u8
    }

    /// Returns the standard chemical symbol for this element.
    ///
    /// Symbols follow IUPAC conventions: one or two letters with the first
    /// capitalized (e.g., `"H"`, `"He"`, `"Fe"`).
    ///
    /// # Returns
    ///
    /// Static string slice containing the element symbol.
    pub fn symbol(&self) -> &'static str {
        match self {
            Element::H => "H",
            Element::He => "He",
            Element::Li => "Li",
            Element::Be => "Be",
            Element::B => "B",
            Element::C => "C",
            Element::N => "N",
            Element::O => "O",
            Element::F => "F",
            Element::Ne => "Ne",
            Element::Na => "Na",
            Element::Mg => "Mg",
            Element::Al => "Al",
            Element::Si => "Si",
            Element::P => "P",
            Element::S => "S",
            Element::Cl => "Cl",
            Element::Ar => "Ar",
            Element::K => "K",
            Element::Ca => "Ca",
            Element::Sc => "Sc",
            Element::Ti => "Ti",
            Element::V => "V",
            Element::Cr => "Cr",
            Element::Mn => "Mn",
            Element::Fe => "Fe",
            Element::Co => "Co",
            Element::Ni => "Ni",
            Element::Cu => "Cu",
            Element::Zn => "Zn",
            Element::Ga => "Ga",
            Element::Ge => "Ge",
            Element::As => "As",
            Element::Se => "Se",
            Element::Br => "Br",
            Element::Kr => "Kr",
            Element::Rb => "Rb",
            Element::Sr => "Sr",
            Element::Y => "Y",
            Element::Zr => "Zr",
            Element::Nb => "Nb",
            Element::Mo => "Mo",
            Element::Tc => "Tc",
            Element::Ru => "Ru",
            Element::Rh => "Rh",
            Element::Pd => "Pd",
            Element::Ag => "Ag",
            Element::Cd => "Cd",
            Element::In => "In",
            Element::Sn => "Sn",
            Element::Sb => "Sb",
            Element::Te => "Te",
            Element::I => "I",
            Element::Xe => "Xe",
            Element::Cs => "Cs",
            Element::Ba => "Ba",
            Element::La => "La",
            Element::Ce => "Ce",
            Element::Pr => "Pr",
            Element::Nd => "Nd",
            Element::Pm => "Pm",
            Element::Sm => "Sm",
            Element::Eu => "Eu",
            Element::Gd => "Gd",
            Element::Tb => "Tb",
            Element::Dy => "Dy",
            Element::Ho => "Ho",
            Element::Er => "Er",
            Element::Tm => "Tm",
            Element::Yb => "Yb",
            Element::Lu => "Lu",
            Element::Hf => "Hf",
            Element::Ta => "Ta",
            Element::W => "W",
            Element::Re => "Re",
            Element::Os => "Os",
            Element::Ir => "Ir",
            Element::Pt => "Pt",
            Element::Au => "Au",
            Element::Hg => "Hg",
            Element::Tl => "Tl",
            Element::Pb => "Pb",
            Element::Bi => "Bi",
            Element::Po => "Po",
            Element::At => "At",
            Element::Rn => "Rn",
            Element::Fr => "Fr",
            Element::Ra => "Ra",
            Element::Ac => "Ac",
            Element::Th => "Th",
            Element::Pa => "Pa",
            Element::U => "U",
            Element::Np => "Np",
            Element::Pu => "Pu",
            Element::Am => "Am",
            Element::Cm => "Cm",
            Element::Bk => "Bk",
            Element::Cf => "Cf",
            Element::Es => "Es",
            Element::Fm => "Fm",
            Element::Md => "Md",
            Element::No => "No",
            Element::Lr => "Lr",
            Element::Rf => "Rf",
            Element::Db => "Db",
            Element::Sg => "Sg",
            Element::Bh => "Bh",
            Element::Hs => "Hs",
            Element::Mt => "Mt",
            Element::Ds => "Ds",
            Element::Rg => "Rg",
            Element::Cn => "Cn",
            Element::Nh => "Nh",
            Element::Fl => "Fl",
            Element::Mc => "Mc",
            Element::Lv => "Lv",
            Element::Ts => "Ts",
            Element::Og => "Og",
        }
    }
}

impl fmt::Display for Element {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str(self.symbol())
    }
}

impl FromStr for Element {
    type Err = ParseElementError;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s {
            "H" => Ok(Element::H),
            "He" => Ok(Element::He),
            "Li" => Ok(Element::Li),
            "Be" => Ok(Element::Be),
            "B" => Ok(Element::B),
            "C" => Ok(Element::C),
            "N" => Ok(Element::N),
            "O" => Ok(Element::O),
            "F" => Ok(Element::F),
            "Ne" => Ok(Element::Ne),
            "Na" => Ok(Element::Na),
            "Mg" => Ok(Element::Mg),
            "Al" => Ok(Element::Al),
            "Si" => Ok(Element::Si),
            "P" => Ok(Element::P),
            "S" => Ok(Element::S),
            "Cl" => Ok(Element::Cl),
            "Ar" => Ok(Element::Ar),
            "K" => Ok(Element::K),
            "Ca" => Ok(Element::Ca),
            "Sc" => Ok(Element::Sc),
            "Ti" => Ok(Element::Ti),
            "V" => Ok(Element::V),
            "Cr" => Ok(Element::Cr),
            "Mn" => Ok(Element::Mn),
            "Fe" => Ok(Element::Fe),
            "Co" => Ok(Element::Co),
            "Ni" => Ok(Element::Ni),
            "Cu" => Ok(Element::Cu),
            "Zn" => Ok(Element::Zn),
            "Ga" => Ok(Element::Ga),
            "Ge" => Ok(Element::Ge),
            "As" => Ok(Element::As),
            "Se" => Ok(Element::Se),
            "Br" => Ok(Element::Br),
            "Kr" => Ok(Element::Kr),
            "Rb" => Ok(Element::Rb),
            "Sr" => Ok(Element::Sr),
            "Y" => Ok(Element::Y),
            "Zr" => Ok(Element::Zr),
            "Nb" => Ok(Element::Nb),
            "Mo" => Ok(Element::Mo),
            "Tc" => Ok(Element::Tc),
            "Ru" => Ok(Element::Ru),
            "Rh" => Ok(Element::Rh),
            "Pd" => Ok(Element::Pd),
            "Ag" => Ok(Element::Ag),
            "Cd" => Ok(Element::Cd),
            "In" => Ok(Element::In),
            "Sn" => Ok(Element::Sn),
            "Sb" => Ok(Element::Sb),
            "Te" => Ok(Element::Te),
            "I" => Ok(Element::I),
            "Xe" => Ok(Element::Xe),
            "Cs" => Ok(Element::Cs),
            "Ba" => Ok(Element::Ba),
            "La" => Ok(Element::La),
            "Ce" => Ok(Element::Ce),
            "Pr" => Ok(Element::Pr),
            "Nd" => Ok(Element::Nd),
            "Pm" => Ok(Element::Pm),
            "Sm" => Ok(Element::Sm),
            "Eu" => Ok(Element::Eu),
            "Gd" => Ok(Element::Gd),
            "Tb" => Ok(Element::Tb),
            "Dy" => Ok(Element::Dy),
            "Ho" => Ok(Element::Ho),
            "Er" => Ok(Element::Er),
            "Tm" => Ok(Element::Tm),
            "Yb" => Ok(Element::Yb),
            "Lu" => Ok(Element::Lu),
            "Hf" => Ok(Element::Hf),
            "Ta" => Ok(Element::Ta),
            "W" => Ok(Element::W),
            "Re" => Ok(Element::Re),
            "Os" => Ok(Element::Os),
            "Ir" => Ok(Element::Ir),
            "Pt" => Ok(Element::Pt),
            "Au" => Ok(Element::Au),
            "Hg" => Ok(Element::Hg),
            "Tl" => Ok(Element::Tl),
            "Pb" => Ok(Element::Pb),
            "Bi" => Ok(Element::Bi),
            "Po" => Ok(Element::Po),
            "At" => Ok(Element::At),
            "Rn" => Ok(Element::Rn),
            "Fr" => Ok(Element::Fr),
            "Ra" => Ok(Element::Ra),
            "Ac" => Ok(Element::Ac),
            "Th" => Ok(Element::Th),
            "Pa" => Ok(Element::Pa),
            "U" => Ok(Element::U),
            "Np" => Ok(Element::Np),
            "Pu" => Ok(Element::Pu),
            "Am" => Ok(Element::Am),
            "Cm" => Ok(Element::Cm),
            "Bk" => Ok(Element::Bk),
            "Cf" => Ok(Element::Cf),
            "Es" => Ok(Element::Es),
            "Fm" => Ok(Element::Fm),
            "Md" => Ok(Element::Md),
            "No" => Ok(Element::No),
            "Lr" => Ok(Element::Lr),
            "Rf" => Ok(Element::Rf),
            "Db" => Ok(Element::Db),
            "Sg" => Ok(Element::Sg),
            "Bh" => Ok(Element::Bh),
            "Hs" => Ok(Element::Hs),
            "Mt" => Ok(Element::Mt),
            "Ds" => Ok(Element::Ds),
            "Rg" => Ok(Element::Rg),
            "Cn" => Ok(Element::Cn),
            "Nh" => Ok(Element::Nh),
            "Fl" => Ok(Element::Fl),
            "Mc" => Ok(Element::Mc),
            "Lv" => Ok(Element::Lv),
            "Ts" => Ok(Element::Ts),
            "Og" => Ok(Element::Og),
            _ => Err(ParseElementError(s.to_string())),
        }
    }
}

/// Bond order classification for chemical bonds.
///
/// Represents the multiplicity or aromaticity of a covalent bond.
/// Used to determine equilibrium bond lengths and force constants
/// in the DREIDING force field parameterization.
///
/// # Examples
///
/// ```
/// use dreid_forge::BondOrder;
/// use std::str::FromStr;
///
/// // Parse from string representation
/// let single = BondOrder::from_str("single").unwrap();
/// let aromatic = BondOrder::from_str("ar").unwrap();
///
/// assert_eq!(single.value(), 1.0);
/// assert_eq!(aromatic.value(), 1.5);
/// ```
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum BondOrder {
    /// Single bond with bond order 1.0.
    Single,
    /// Double bond with bond order 2.0.
    Double,
    /// Triple bond with bond order 3.0.
    Triple,
    /// Aromatic bond with effective bond order 1.5.
    Aromatic,
}

impl BondOrder {
    /// Returns the numeric bond order value.
    ///
    /// Converts the bond order variant to its corresponding
    /// floating-point multiplicity value used in force field
    /// parameter calculations.
    ///
    /// # Returns
    ///
    /// * `1.0` for single bonds
    /// * `2.0` for double bonds
    /// * `3.0` for triple bonds
    /// * `1.5` for aromatic bonds
    pub fn value(&self) -> f64 {
        match self {
            BondOrder::Single => 1.0,
            BondOrder::Double => 2.0,
            BondOrder::Triple => 3.0,
            BondOrder::Aromatic => 1.5,
        }
    }
}

/// Formats the bond order as a human-readable string.
///
/// Produces title-case output: "Single", "Double", "Triple", or "Aromatic".
impl fmt::Display for BondOrder {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            BondOrder::Single => write!(f, "Single"),
            BondOrder::Double => write!(f, "Double"),
            BondOrder::Triple => write!(f, "Triple"),
            BondOrder::Aromatic => write!(f, "Aromatic"),
        }
    }
}

/// Parses a bond order from its string representation.
///
/// Accepts case-insensitive text or numeric forms:
/// * "single" or "1" → `Single`
/// * "double" or "2" → `Double`
/// * "triple" or "3" → `Triple`
/// * "aromatic" or "ar" → `Aromatic`
///
/// # Errors
///
/// Returns [`ParseBondOrderError`] if the input string does not
/// match any recognized bond order format.
impl FromStr for BondOrder {
    type Err = ParseBondOrderError;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s.to_lowercase().as_str() {
            "single" | "1" => Ok(BondOrder::Single),
            "double" | "2" => Ok(BondOrder::Double),
            "triple" | "3" => Ok(BondOrder::Triple),
            "aromatic" | "ar" => Ok(BondOrder::Aromatic),
            _ => Err(ParseBondOrderError(s.to_string())),
        }
    }
}

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

    fn approx_eq(a: f64, b: f64, eps: f64) -> bool {
        (a - b).abs() <= eps
    }

    #[test]
    fn element_from_str_valid() {
        assert_eq!(Element::from_str("H").unwrap(), Element::H);
        assert_eq!(Element::from_str("He").unwrap(), Element::He);
        assert_eq!(Element::from_str("Fe").unwrap(), Element::Fe);
        assert_eq!(Element::from_str("Og").unwrap(), Element::Og);
    }

    #[test]
    fn element_from_str_invalid_case() {
        let err = Element::from_str("h").unwrap_err();
        let s = format!("{}", err);
        assert_eq!(s, "invalid or unsupported element symbol: 'h'");
    }

    #[test]
    fn element_symbol_display_and_atomic_number() {
        let el = Element::Na;
        assert_eq!(el.symbol(), "Na");
        assert_eq!(el.to_string(), "Na");
        assert_eq!(el.atomic_number(), 11u8);
    }

    #[test]
    fn atomic_mass_values() {
        assert!(approx_eq(Element::H.atomic_mass(), 1.008, 1e-6));
        assert!(approx_eq(Element::C.atomic_mass(), 12.011, 1e-6));
        assert!(approx_eq(Element::Fe.atomic_mass(), 55.845, 1e-6));
        assert!(approx_eq(Element::Og.atomic_mass(), 294.0, 1e-6));
    }

    #[test]
    fn bondorder_from_str_variants() {
        assert_eq!(BondOrder::from_str("single").unwrap(), BondOrder::Single);
        assert_eq!(BondOrder::from_str("1").unwrap(), BondOrder::Single);
        assert_eq!(BondOrder::from_str("Double").unwrap(), BondOrder::Double);
        assert_eq!(BondOrder::from_str("2").unwrap(), BondOrder::Double);
        assert_eq!(BondOrder::from_str("triple").unwrap(), BondOrder::Triple);
        assert_eq!(BondOrder::from_str("3").unwrap(), BondOrder::Triple);
        assert_eq!(BondOrder::from_str("AR").unwrap(), BondOrder::Aromatic);
        assert_eq!(
            BondOrder::from_str("aromatic").unwrap(),
            BondOrder::Aromatic
        );
    }

    #[test]
    fn bondorder_from_str_invalid() {
        let err = BondOrder::from_str("quad").unwrap_err();
        let s = format!("{}", err);
        assert_eq!(s, "invalid bond order string: 'quad'");
    }

    #[test]
    fn bondorder_value_and_display() {
        assert!(approx_eq(BondOrder::Single.value(), 1.0, 1e-12));
        assert!(approx_eq(BondOrder::Double.value(), 2.0, 1e-12));
        assert!(approx_eq(BondOrder::Triple.value(), 3.0, 1e-12));
        assert!(approx_eq(BondOrder::Aromatic.value(), 1.5, 1e-12));

        assert_eq!(BondOrder::Single.to_string(), "Single");
        assert_eq!(BondOrder::Double.to_string(), "Double");
        assert_eq!(BondOrder::Triple.to_string(), "Triple");
        assert_eq!(BondOrder::Aromatic.to_string(), "Aromatic");
    }
}
