//! Degrees-of-freedom labels declared by components.
//!
//! Each residual equation contributed by a [`crate::Component`] should eventually
//! carry a semantic [`EquationRole`]. The solver aggregates these roles into a
//! system-wide DoF ledger and enforces:
//!
//! ```text
//! n_equations == n_unknowns
//! ```
//!
//! # Fix / Free rule
//!
//! Adding an equation (a *fix*) without freeing an unknown (or dropping another
//! residual) makes the system over-constrained. Outlet closures such as quality
//! or superheat are typical DoF consumers and must be paired with a free
//! actuator (EXV opening, compressor speed, level control, …).

use std::fmt;

/// What a residual equation is responsible for closing.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum EquationRole {
    /// Mass conservation on a named stream (`"refrigerant"`, `"secondary"`, …).
    MassConservation {
        /// Stream label.
        stream: &'static str,
    },
    /// Momentum / pressure-drop relation (ΔP or P_out − P_in − f(ṁ)).
    MomentumOrPressureDrop {
        /// Stream label.
        stream: &'static str,
    },
    /// Energy balance on a stream (`ṁ Δh ± Q = 0`).
    EnergyBalance {
        /// Stream label.
        stream: &'static str,
    },
    /// Outlet thermo closure that pins a state (SH, SC, quality, level…).
    OutletClosure {
        /// Closure kind label (`"superheat"`, `"subcooling"`, `"quality"`, …).
        kind: &'static str,
    },
    /// Boundary Dirichlet residual (source/sink imposed P, h, or ṁ).
    BoundaryDirichlet {
        /// Quantity label (`"P"`, `"h"`, `"m"`).
        quantity: &'static str,
    },
    /// Closing residual for a free actuator owned by the component.
    ActuatorClosure {
        /// Actuator name.
        name: &'static str,
    },
    /// Thermal-coupling duty residual at system level.
    CouplingDuty,
    /// Inverse-control tracking residual `measure − target`.
    ControlTracking {
        /// Constraint / control name.
        name: String,
    },
    /// Saturated-PI residual pair entry.
    SaturatedControl {
        /// Role within the pair (`"actuator_law"` or `"tracking"`).
        role: &'static str,
    },
    /// Pass-through continuity (Anchor, reversing valve inline, …).
    Continuity {
        /// Quantity label (`"P"` or `"h"`).
        quantity: &'static str,
    },
    /// Legacy placeholder when a component has not declared roles yet.
    Unspecified {
        /// Local residual index inside the component block.
        local_index: usize,
    },
}

impl fmt::Display for EquationRole {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::MassConservation { stream } => write!(f, "mass[{stream}]"),
            Self::MomentumOrPressureDrop { stream } => write!(f, "momentum[{stream}]"),
            Self::EnergyBalance { stream } => write!(f, "energy[{stream}]"),
            Self::OutletClosure { kind } => write!(f, "outlet_closure[{kind}]"),
            Self::BoundaryDirichlet { quantity } => write!(f, "boundary[{quantity}]"),
            Self::ActuatorClosure { name } => write!(f, "actuator[{name}]"),
            Self::CouplingDuty => write!(f, "coupling_duty"),
            Self::ControlTracking { name } => write!(f, "control[{name}]"),
            Self::SaturatedControl { role } => write!(f, "saturated[{role}]"),
            Self::Continuity { quantity } => write!(f, "continuity[{quantity}]"),
            Self::Unspecified { local_index } => write!(f, "unspecified[{local_index}]"),
        }
    }
}

/// Builds unspecified roles when a component has not migrated yet.
pub fn unspecified_roles(n: usize) -> Vec<EquationRole> {
    (0..n)
        .map(|local_index| EquationRole::Unspecified { local_index })
        .collect()
}

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

    #[test]
    fn display_energy() {
        let r = EquationRole::EnergyBalance {
            stream: "secondary",
        };
        assert_eq!(r.to_string(), "energy[secondary]");
    }
}
