//! System state container for thermodynamic simulations.
//!
//! This module provides [`SystemState`], a type-safe container for the thermodynamic
//! state variables of a system during simulation. Each edge in the system graph
//! has two state variables: pressure and enthalpy.

use crate::{Enthalpy, Pressure};
use serde::{Deserialize, Serialize};
use std::ops::{Deref, DerefMut, Index, IndexMut};

/// Error returned when constructing `SystemState` with invalid data length.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct InvalidStateLengthError {
    /// The actual length of the provided vector.
    pub actual_length: usize,
}

impl std::fmt::Display for InvalidStateLengthError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(
            f,
            "Data length must be even (P, h pairs), got {}",
            self.actual_length
        )
    }
}

impl std::error::Error for InvalidStateLengthError {}

/// Represents the thermodynamic state of the entire system.
///
/// The internal layout is `[P_edge0, h_edge0, P_edge1, h_edge1, ...]` where:
/// - `P`: Pressure in Pascals (Pa)
/// - `h`: Specific enthalpy in Joules per kilogram (J/kg)
///
/// Each edge in the system graph corresponds to a connection between two ports
/// and carries exactly two state variables.
///
/// # Example
///
/// ```
/// use entropyk_core::{SystemState, Pressure, Enthalpy};
///
/// // Create a state for a system with 3 edges
/// let mut state = SystemState::new(3);
/// assert_eq!(state.edge_count(), 3);
///
/// // Set values for edge 0
/// state.set_pressure(0, Pressure::from_bar(2.0));
/// state.set_enthalpy(0, Enthalpy::from_kilojoules_per_kg(400.0));
///
/// // Retrieve values
/// let p = state.pressure(0).unwrap();
/// let h = state.enthalpy(0).unwrap();
/// assert_eq!(p.to_bar(), 2.0);
/// assert_eq!(h.to_kilojoules_per_kg(), 400.0);
/// ```
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SystemState {
    #[serde(alias = "data")]
    data: Vec<f64>,
    #[serde(alias = "edge_count")]
    edge_count: usize,
}

impl SystemState {
    /// Creates a new `SystemState` with all values initialized to zero.
    ///
    /// # Arguments
    ///
    /// * `edge_count` - Number of edges in the system. Total storage is `2 * edge_count`.
    ///
    /// # Example
    ///
    /// ```
    /// use entropyk_core::SystemState;
    ///
    /// let state = SystemState::new(5);
    /// assert_eq!(state.edge_count(), 5);
    /// assert_eq!(state.as_slice().len(), 10); // 2 values per edge
    /// ```
    pub fn new(edge_count: usize) -> Self {
        Self {
            data: vec![0.0; edge_count * 2],
            edge_count,
        }
    }

    /// Creates a `SystemState` from a raw vector of values.
    ///
    /// # Arguments
    ///
    /// * `data` - Raw vector with layout `[P0, h0, P1, h1, ...]`
    ///
    /// # Panics
    ///
    /// Panics if `data.len()` is not even (each edge needs exactly 2 values).
    ///
    /// # Example
    ///
    /// ```
    /// use entropyk_core::SystemState;
    ///
    /// let data = vec![100000.0, 400000.0, 200000.0, 250000.0];
    /// let state = SystemState::from_vec(data);
    /// assert_eq!(state.edge_count(), 2);
    /// ```
    pub fn from_vec(data: Vec<f64>) -> Self {
        assert!(
            data.len().is_multiple_of(2),
            "Data length must be even (P, h pairs), got {}",
            data.len()
        );
        let edge_count = data.len() / 2;
        Self { data, edge_count }
    }

    /// Creates a `SystemState` from a raw vector, returning an error on invalid length.
    ///
    /// # Arguments
    ///
    /// * `data` - Raw vector with layout `[P0, h0, P1, h1, ...]`
    ///
    /// # Errors
    ///
    /// Returns `Err(InvalidStateLengthError)` if `data.len()` is not even.
    ///
    /// # Example
    ///
    /// ```
    /// use entropyk_core::SystemState;
    ///
    /// let data = vec![100000.0, 400000.0, 200000.0, 250000.0];
    /// let state = SystemState::try_from_vec(data);
    /// assert!(state.is_ok());
    ///
    /// let bad_data = vec![1.0, 2.0, 3.0];
    /// assert!(SystemState::try_from_vec(bad_data).is_err());
    /// ```
    pub fn try_from_vec(data: Vec<f64>) -> Result<Self, InvalidStateLengthError> {
        if !data.len().is_multiple_of(2) {
            return Err(InvalidStateLengthError {
                actual_length: data.len(),
            });
        }
        let edge_count = data.len() / 2;
        Ok(Self { data, edge_count })
    }

    /// Returns the number of edges in the system.
    pub fn edge_count(&self) -> usize {
        self.edge_count
    }

    /// Returns the pressure at the specified edge.
    ///
    /// Returns `None` if `edge_idx` is out of bounds.
    ///
    /// # Example
    ///
    /// ```
    /// use entropyk_core::{SystemState, Pressure};
    ///
    /// let mut state = SystemState::new(2);
    /// state.set_pressure(0, Pressure::from_pascals(100000.0));
    ///
    /// let p = state.pressure(0).unwrap();
    /// assert_eq!(p.to_pascals(), 100000.0);
    ///
    /// // Out of bounds returns None
    /// assert!(state.pressure(5).is_none());
    /// ```
    pub fn pressure(&self, edge_idx: usize) -> Option<Pressure> {
        self.data
            .get(edge_idx * 2)
            .map(|&p| Pressure::from_pascals(p))
    }

    /// Returns the enthalpy at the specified edge.
    ///
    /// Returns `None` if `edge_idx` is out of bounds.
    ///
    /// # Example
    ///
    /// ```
    /// use entropyk_core::{SystemState, Enthalpy};
    ///
    /// let mut state = SystemState::new(2);
    /// state.set_enthalpy(1, Enthalpy::from_joules_per_kg(300000.0));
    ///
    /// let h = state.enthalpy(1).unwrap();
    /// assert_eq!(h.to_joules_per_kg(), 300000.0);
    /// ```
    pub fn enthalpy(&self, edge_idx: usize) -> Option<Enthalpy> {
        self.data
            .get(edge_idx * 2 + 1)
            .map(|&h| Enthalpy::from_joules_per_kg(h))
    }

    /// Sets the pressure at the specified edge.
    ///
    /// # Panics
    ///
    /// Panics in debug mode if `edge_idx` is out of bounds. In release mode,
    /// silently does nothing.
    ///
    /// # Example
    ///
    /// ```
    /// use entropyk_core::{SystemState, Pressure};
    ///
    /// let mut state = SystemState::new(2);
    /// state.set_pressure(0, Pressure::from_bar(1.5));
    ///
    /// assert_eq!(state.pressure(0).unwrap().to_bar(), 1.5);
    /// ```
    #[track_caller]
    pub fn set_pressure(&mut self, edge_idx: usize, p: Pressure) {
        debug_assert!(
            edge_idx < self.edge_count,
            "set_pressure: edge_idx {} out of bounds (edge_count: {})",
            edge_idx,
            self.edge_count
        );
        if let Some(slot) = self.data.get_mut(edge_idx * 2) {
            *slot = p.to_pascals();
        }
    }

    /// Sets the enthalpy at the specified edge.
    ///
    /// # Panics
    ///
    /// Panics in debug mode if `edge_idx` is out of bounds. In release mode,
    /// silently does nothing.
    ///
    /// # Example
    ///
    /// ```
    /// use entropyk_core::{SystemState, Enthalpy};
    ///
    /// let mut state = SystemState::new(2);
    /// state.set_enthalpy(0, Enthalpy::from_kilojoules_per_kg(250.0));
    ///
    /// assert_eq!(state.enthalpy(0).unwrap().to_kilojoules_per_kg(), 250.0);
    /// ```
    #[track_caller]
    pub fn set_enthalpy(&mut self, edge_idx: usize, h: Enthalpy) {
        debug_assert!(
            edge_idx < self.edge_count,
            "set_enthalpy: edge_idx {} out of bounds (edge_count: {})",
            edge_idx,
            self.edge_count
        );
        if let Some(slot) = self.data.get_mut(edge_idx * 2 + 1) {
            *slot = h.to_joules_per_kg();
        }
    }

    /// Returns a slice of the raw data.
    ///
    /// Layout: `[P0, h0, P1, h1, ...]`
    pub fn as_slice(&self) -> &[f64] {
        &self.data
    }

    /// Returns a mutable slice of the raw data.
    ///
    /// Layout: `[P0, h0, P1, h1, ...]`
    pub fn as_mut_slice(&mut self) -> &mut [f64] {
        &mut self.data
    }

    /// Consumes the `SystemState` and returns the underlying vector.
    ///
    /// # Example
    ///
    /// ```
    /// use entropyk_core::SystemState;
    ///
    /// let state = SystemState::new(2);
    /// let data = state.into_vec();
    /// assert_eq!(data.len(), 4);
    /// ```
    pub fn into_vec(self) -> Vec<f64> {
        self.data
    }

    /// Returns a cloned copy of the underlying vector.
    ///
    /// # Example
    ///
    /// ```
    /// use entropyk_core::SystemState;
    ///
    /// let mut state = SystemState::new(2);
    /// state.set_pressure(0, entropyk_core::Pressure::from_pascals(100000.0));
    /// let data = state.to_vec();
    /// assert_eq!(data.len(), 4);
    /// assert_eq!(data[0], 100000.0);
    /// ```
    pub fn to_vec(&self) -> Vec<f64> {
        self.data.clone()
    }

    /// Iterates over all edges, yielding `(Pressure, Enthalpy)` pairs.
    ///
    /// # Example
    ///
    /// ```
    /// use entropyk_core::{SystemState, Pressure, Enthalpy};
    ///
    /// let mut state = SystemState::new(2);
    /// state.set_pressure(0, Pressure::from_pascals(100000.0));
    /// state.set_enthalpy(0, Enthalpy::from_joules_per_kg(300000.0));
    /// state.set_pressure(1, Pressure::from_pascals(200000.0));
    /// state.set_enthalpy(1, Enthalpy::from_joules_per_kg(400000.0));
    ///
    /// let edges: Vec<_> = state.iter_edges().collect();
    /// assert_eq!(edges.len(), 2);
    /// assert_eq!(edges[0].0.to_pascals(), 100000.0);
    /// assert_eq!(edges[1].0.to_pascals(), 200000.0);
    /// ```
    pub fn iter_edges(&self) -> impl Iterator<Item = (Pressure, Enthalpy)> + '_ {
        self.data.chunks_exact(2).map(|chunk| {
            (
                Pressure::from_pascals(chunk[0]),
                Enthalpy::from_joules_per_kg(chunk[1]),
            )
        })
    }

    /// Returns the total number of state variables (2 per edge).
    pub fn len(&self) -> usize {
        self.data.len()
    }

    /// Returns `true` if the state contains no edges.
    pub fn is_empty(&self) -> bool {
        self.edge_count == 0
    }
}

impl Default for SystemState {
    fn default() -> Self {
        Self::new(0)
    }
}

impl AsRef<[f64]> for SystemState {
    fn as_ref(&self) -> &[f64] {
        &self.data
    }
}

impl AsMut<[f64]> for SystemState {
    fn as_mut(&mut self) -> &mut [f64] {
        &mut self.data
    }
}

impl From<Vec<f64>> for SystemState {
    fn from(data: Vec<f64>) -> Self {
        Self::from_vec(data)
    }
}

impl From<SystemState> for Vec<f64> {
    fn from(state: SystemState) -> Self {
        state.into_vec()
    }
}

impl Index<usize> for SystemState {
    type Output = f64;

    fn index(&self, index: usize) -> &Self::Output {
        &self.data[index]
    }
}

impl IndexMut<usize> for SystemState {
    fn index_mut(&mut self, index: usize) -> &mut Self::Output {
        &mut self.data[index]
    }
}

impl Deref for SystemState {
    type Target = [f64];

    fn deref(&self) -> &Self::Target {
        &self.data
    }
}

impl DerefMut for SystemState {
    fn deref_mut(&mut self) -> &mut Self::Target {
        &mut self.data
    }
}

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

    #[test]
    fn test_new() {
        let state = SystemState::new(3);
        assert_eq!(state.edge_count(), 3);
        assert_eq!(state.as_slice().len(), 6);
        assert!(!state.is_empty());
    }

    #[test]
    fn test_new_zero_edges() {
        let state = SystemState::new(0);
        assert_eq!(state.edge_count(), 0);
        assert_eq!(state.as_slice().len(), 0);
        assert!(state.is_empty());
    }

    #[test]
    fn test_default() {
        let state = SystemState::default();
        assert_eq!(state.edge_count(), 0);
        assert!(state.is_empty());
    }

    #[test]
    fn test_pressure_access() {
        let mut state = SystemState::new(2);
        state.set_pressure(0, Pressure::from_pascals(101325.0));
        state.set_pressure(1, Pressure::from_pascals(200000.0));

        assert_relative_eq!(
            state.pressure(0).unwrap().to_pascals(),
            101325.0,
            epsilon = 1e-10
        );
        assert_relative_eq!(
            state.pressure(1).unwrap().to_pascals(),
            200000.0,
            epsilon = 1e-10
        );
    }

    #[test]
    fn test_enthalpy_access() {
        let mut state = SystemState::new(2);
        state.set_enthalpy(0, Enthalpy::from_joules_per_kg(400000.0));
        state.set_enthalpy(1, Enthalpy::from_joules_per_kg(250000.0));

        assert_relative_eq!(
            state.enthalpy(0).unwrap().to_joules_per_kg(),
            400000.0,
            epsilon = 1e-10
        );
        assert_relative_eq!(
            state.enthalpy(1).unwrap().to_joules_per_kg(),
            250000.0,
            epsilon = 1e-10
        );
    }

    #[test]
    fn test_out_of_bounds_pressure() {
        let state = SystemState::new(2);
        assert!(state.pressure(2).is_none());
        assert!(state.pressure(100).is_none());
    }

    #[test]
    fn test_out_of_bounds_enthalpy() {
        let state = SystemState::new(2);
        assert!(state.enthalpy(2).is_none());
        assert!(state.enthalpy(100).is_none());
    }

    #[test]
    #[cfg(debug_assertions)]
    #[should_panic(expected = "edge_idx 10 out of bounds")]
    fn test_set_pressure_out_of_bounds_panics_in_debug() {
        let mut state = SystemState::new(2);
        state.set_pressure(10, Pressure::from_pascals(100000.0));
    }

    #[test]
    #[cfg(debug_assertions)]
    #[should_panic(expected = "edge_idx 10 out of bounds")]
    fn test_set_enthalpy_out_of_bounds_panics_in_debug() {
        let mut state = SystemState::new(2);
        state.set_enthalpy(10, Enthalpy::from_joules_per_kg(300000.0));
    }

    #[test]
    fn test_from_vec_valid() {
        let data = vec![101325.0, 400000.0, 200000.0, 250000.0];
        let state = SystemState::from_vec(data);

        assert_eq!(state.edge_count(), 2);
        assert_relative_eq!(
            state.pressure(0).unwrap().to_pascals(),
            101325.0,
            epsilon = 1e-10
        );
        assert_relative_eq!(
            state.enthalpy(0).unwrap().to_joules_per_kg(),
            400000.0,
            epsilon = 1e-10
        );
        assert_relative_eq!(
            state.pressure(1).unwrap().to_pascals(),
            200000.0,
            epsilon = 1e-10
        );
        assert_relative_eq!(
            state.enthalpy(1).unwrap().to_joules_per_kg(),
            250000.0,
            epsilon = 1e-10
        );
    }

    #[test]
    #[should_panic(expected = "Data length must be even")]
    fn test_from_vec_odd_length() {
        let data = vec![1.0, 2.0, 3.0]; // 3 elements = odd
        let _ = SystemState::from_vec(data);
    }

    #[test]
    fn test_from_vec_empty() {
        let data: Vec<f64> = vec![];
        let state = SystemState::from_vec(data);
        assert_eq!(state.edge_count(), 0);
        assert!(state.is_empty());
    }

    #[test]
    fn test_try_from_vec_valid() {
        let data = vec![100000.0, 400000.0, 200000.0, 250000.0];
        let state = SystemState::try_from_vec(data).unwrap();
        assert_eq!(state.edge_count(), 2);
    }

    #[test]
    fn test_try_from_vec_odd_length() {
        let data = vec![1.0, 2.0, 3.0];
        let err = SystemState::try_from_vec(data).unwrap_err();
        assert_eq!(err.actual_length, 3);
        assert!(err.to_string().contains("must be even"));
    }

    #[test]
    fn test_try_from_vec_empty() {
        let data: Vec<f64> = vec![];
        let state = SystemState::try_from_vec(data).unwrap();
        assert!(state.is_empty());
    }

    #[test]
    fn test_invalid_state_length_error_display() {
        let err = InvalidStateLengthError { actual_length: 5 };
        let msg = format!("{}", err);
        assert!(msg.contains("5"));
        assert!(msg.contains("must be even"));
    }

    #[test]
    fn test_serde_roundtrip() {
        let mut state = SystemState::new(2);
        state.set_pressure(0, Pressure::from_pascals(100000.0));
        state.set_enthalpy(0, Enthalpy::from_joules_per_kg(200000.0));

        let json = serde_json::to_string(&state).unwrap();
        let deserialized: SystemState = serde_json::from_str(&json).unwrap();
        assert_eq!(state, deserialized);
    }

    #[test]
    fn test_iter_edges() {
        let mut state = SystemState::new(2);
        state.set_pressure(0, Pressure::from_pascals(100000.0));
        state.set_enthalpy(0, Enthalpy::from_joules_per_kg(300000.0));
        state.set_pressure(1, Pressure::from_pascals(200000.0));
        state.set_enthalpy(1, Enthalpy::from_joules_per_kg(400000.0));

        let edges: Vec<_> = state.iter_edges().collect();
        assert_eq!(edges.len(), 2);
        assert_relative_eq!(edges[0].0.to_pascals(), 100000.0, epsilon = 1e-10);
        assert_relative_eq!(edges[0].1.to_joules_per_kg(), 300000.0, epsilon = 1e-10);
        assert_relative_eq!(edges[1].0.to_pascals(), 200000.0, epsilon = 1e-10);
        assert_relative_eq!(edges[1].1.to_joules_per_kg(), 400000.0, epsilon = 1e-10);
    }

    #[test]
    fn test_iter_edges_empty() {
        let state = SystemState::new(0);
        let edges: Vec<_> = state.iter_edges().collect();
        assert!(edges.is_empty());
    }

    #[test]
    fn test_as_slice() {
        let mut state = SystemState::new(2);
        state.set_pressure(0, Pressure::from_pascals(100000.0));
        state.set_enthalpy(0, Enthalpy::from_joules_per_kg(200000.0));
        state.set_pressure(1, Pressure::from_pascals(300000.0));
        state.set_enthalpy(1, Enthalpy::from_joules_per_kg(400000.0));

        let slice = state.as_slice();
        assert_eq!(slice.len(), 4);
        assert_relative_eq!(slice[0], 100000.0, epsilon = 1e-10);
        assert_relative_eq!(slice[1], 200000.0, epsilon = 1e-10);
        assert_relative_eq!(slice[2], 300000.0, epsilon = 1e-10);
        assert_relative_eq!(slice[3], 400000.0, epsilon = 1e-10);
    }

    #[test]
    fn test_as_mut_slice() {
        let mut state = SystemState::new(2);
        let slice = state.as_mut_slice();
        slice[0] = 100000.0;
        slice[1] = 200000.0;
        slice[2] = 300000.0;
        slice[3] = 400000.0;

        assert_relative_eq!(
            state.pressure(0).unwrap().to_pascals(),
            100000.0,
            epsilon = 1e-10
        );
        assert_relative_eq!(
            state.enthalpy(0).unwrap().to_joules_per_kg(),
            200000.0,
            epsilon = 1e-10
        );
    }

    #[test]
    fn test_into_vec() {
        let mut state = SystemState::new(2);
        state.set_pressure(0, Pressure::from_pascals(100000.0));
        state.set_enthalpy(0, Enthalpy::from_joules_per_kg(200000.0));

        let data = state.into_vec();
        assert_eq!(data.len(), 4);
        assert_relative_eq!(data[0], 100000.0, epsilon = 1e-10);
        assert_relative_eq!(data[1], 200000.0, epsilon = 1e-10);
    }

    #[test]
    fn test_from_vec_conversion() {
        let data = vec![100000.0, 200000.0, 300000.0, 400000.0];
        let state: SystemState = data.into();
        assert_eq!(state.edge_count(), 2);
    }

    #[test]
    fn test_into_vec_conversion() {
        let mut state = SystemState::new(1);
        state.set_pressure(0, Pressure::from_pascals(100000.0));
        state.set_enthalpy(0, Enthalpy::from_joules_per_kg(200000.0));

        let data: Vec<f64> = state.into();
        assert_eq!(data, vec![100000.0, 200000.0]);
    }

    #[test]
    fn test_as_ref_trait() {
        let mut state = SystemState::new(2);
        state.set_pressure(0, Pressure::from_pascals(100000.0));

        let state_ref: &[f64] = state.as_ref();
        assert_relative_eq!(state_ref[0], 100000.0, epsilon = 1e-10);
    }

    #[test]
    fn test_as_mut_trait() {
        let mut state = SystemState::new(2);
        let state_mut: &mut [f64] = state.as_mut();
        state_mut[0] = 500000.0;

        assert_relative_eq!(
            state.pressure(0).unwrap().to_pascals(),
            500000.0,
            epsilon = 1e-10
        );
    }

    #[test]
    fn test_clone() {
        let mut state = SystemState::new(2);
        state.set_pressure(0, Pressure::from_pascals(100000.0));
        state.set_enthalpy(0, Enthalpy::from_joules_per_kg(200000.0));

        let cloned = state.clone();
        assert_eq!(state.edge_count(), cloned.edge_count());
        assert_relative_eq!(
            cloned.pressure(0).unwrap().to_pascals(),
            100000.0,
            epsilon = 1e-10
        );
    }

    #[test]
    fn test_eq() {
        let mut state1 = SystemState::new(2);
        state1.set_pressure(0, Pressure::from_pascals(100000.0));
        state1.set_enthalpy(0, Enthalpy::from_joules_per_kg(200000.0));

        let mut state2 = SystemState::new(2);
        state2.set_pressure(0, Pressure::from_pascals(100000.0));
        state2.set_enthalpy(0, Enthalpy::from_joules_per_kg(200000.0));

        assert_eq!(state1, state2);

        state2.set_pressure(1, Pressure::from_pascals(1.0));
        assert_ne!(state1, state2);
    }

    #[test]
    fn test_len() {
        let state = SystemState::new(5);
        assert_eq!(state.len(), 10);
    }

    #[test]
    fn test_index_trait() {
        let mut state = SystemState::new(2);
        state.set_pressure(0, Pressure::from_pascals(100000.0));
        state.set_enthalpy(0, Enthalpy::from_joules_per_kg(200000.0));
        state.set_pressure(1, Pressure::from_pascals(300000.0));
        state.set_enthalpy(1, Enthalpy::from_joules_per_kg(400000.0));

        // Test Index trait
        assert_relative_eq!(state[0], 100000.0, epsilon = 1e-10);
        assert_relative_eq!(state[1], 200000.0, epsilon = 1e-10);
        assert_relative_eq!(state[2], 300000.0, epsilon = 1e-10);
        assert_relative_eq!(state[3], 400000.0, epsilon = 1e-10);
    }

    #[test]
    fn test_index_mut_trait() {
        let mut state = SystemState::new(2);

        // Test IndexMut trait
        state[0] = 100000.0;
        state[1] = 200000.0;
        state[2] = 300000.0;
        state[3] = 400000.0;

        assert_relative_eq!(
            state.pressure(0).unwrap().to_pascals(),
            100000.0,
            epsilon = 1e-10
        );
        assert_relative_eq!(
            state.enthalpy(0).unwrap().to_joules_per_kg(),
            200000.0,
            epsilon = 1e-10
        );
        assert_relative_eq!(
            state.pressure(1).unwrap().to_pascals(),
            300000.0,
            epsilon = 1e-10
        );
        assert_relative_eq!(
            state.enthalpy(1).unwrap().to_joules_per_kg(),
            400000.0,
            epsilon = 1e-10
        );
    }
}
