//! Thread-local LRU cache for fluid property queries.
//!
//! Avoids redundant backend calls without mutex contention by using
//! per-thread storage. Cache keys use quantized state values since f64
//! does not implement Hash.
//!
//! # Quantization Strategy
//!
//! State values (P, T, h, s, x) are quantized to 1e-9 relative precision
//! for cache key derivation. Solver iterations often repeat the same
//! (P,T) or (P,h) states; quantization should not lose cache hits for
//! typical thermodynamic ranges (P: 1e3–1e7 Pa, T: 200–600 K).

use crate::mixture::Mixture;
use crate::types::{FluidId, Property, FluidState};
use lru::LruCache;
use std::cell::RefCell;
use std::hash::{Hash, Hasher};
use std::num::NonZeroUsize;

/// Default cache capacity (entries). LRU eviction when exceeded.
pub const DEFAULT_CACHE_CAPACITY: usize = 10_000;

/// Default capacity as NonZeroUsize for LruCache (avoids unwrap in production path).
const DEFAULT_CAP_NONZERO: NonZeroUsize = unsafe { NonZeroUsize::new_unchecked(DEFAULT_CACHE_CAPACITY) };

/// Quantization factor: values rounded to 1e-9 relative.
/// (v * 1e9).round() as i64 for Hash-compatible key.
#[inline]
fn quantize(v: f64) -> i64 {
    if v.is_nan() || v.is_infinite() {
        0
    } else {
        (v * 1e9).round() as i64
    }
}

/// Cache key for fluid property lookups.
///
/// Uses quantized state values since f64 does not implement Hash.
/// Includes backend_id so multiple CachedBackend instances don't mix results.
/// For mixtures, includes a hash of the mixture composition.
#[derive(Clone, Debug)]
pub struct CacheKey {
    backend_id: usize,
    fluid: String,
    property: Property,
    variant: u8,
    p_quantized: i64,
    second_quantized: i64,
    mixture_hash: Option<u64>,
}

impl PartialEq for CacheKey {
    fn eq(&self, other: &Self) -> bool {
        self.backend_id == other.backend_id
            && self.fluid == other.fluid
            && self.property == other.property
            && self.variant == other.variant
            && self.p_quantized == other.p_quantized
            && self.second_quantized == other.second_quantized
            && self.mixture_hash == other.mixture_hash
    }
}

impl Eq for CacheKey {}

impl Hash for CacheKey {
    fn hash<H: Hasher>(&self, state: &mut H) {
        self.backend_id.hash(state);
        self.fluid.hash(state);
        self.property.hash(state);
        self.variant.hash(state);
        self.p_quantized.hash(state);
        self.second_quantized.hash(state);
        self.mixture_hash.hash(state);
    }
}

impl CacheKey {
    /// Build a cache key from fluid, property, state, and backend id.
    pub fn new(backend_id: usize, fluid: &FluidId, property: Property, state: &FluidState) -> Self {
        let (p, second, variant, mixture_hash) = match state {
            FluidState::PressureTemperature(p, t) => (p.to_pascals(), t.to_kelvin(), 0u8, None),
            FluidState::PressureEnthalpy(p, h) => {
                (p.to_pascals(), h.to_joules_per_kg(), 1u8, None)
            }
            FluidState::PressureEntropy(p, s) => {
                (p.to_pascals(), s.to_joules_per_kg_kelvin(), 2u8, None)
            }
            FluidState::PressureQuality(p, x) => (p.to_pascals(), x.value(), 3u8, None),
            FluidState::PressureTemperatureMixture(p, t, ref m) => {
                (p.to_pascals(), t.to_kelvin(), 4u8, Some(mix_hash(m)))
            }
            FluidState::PressureEnthalpyMixture(p, h, ref m) => {
                (p.to_pascals(), h.to_joules_per_kg(), 5u8, Some(mix_hash(m)))
            }
            FluidState::PressureQualityMixture(p, x, ref m) => {
                (p.to_pascals(), x.value(), 6u8, Some(mix_hash(m)))
            }
        };
        CacheKey {
            backend_id,
            fluid: fluid.0.clone(),
            property,
            variant,
            p_quantized: quantize(p),
            second_quantized: quantize(second),
            mixture_hash,
        }
    }
}

/// Compute a simple hash for a mixture for cache key purposes.
fn mix_hash(mixture: &Mixture) -> u64 {
    use std::collections::hash_map::DefaultHasher;
    let mut hasher = DefaultHasher::new();
    mixture.hash(&mut hasher);
    hasher.finish()
}

thread_local! {
    static CACHE: RefCell<LruCache<CacheKey, f64>> = RefCell::new(
        LruCache::new(DEFAULT_CAP_NONZERO)
    );
}

/// Get a value from the thread-local cache (no allocation on key build for hot path).
pub fn cache_get(
    backend_id: usize,
    fluid: &FluidId,
    property: Property,
    state: &FluidState,
) -> Option<f64> {
    let key = CacheKey::new(backend_id, fluid, property, state);
    CACHE.with(|c| {
        let mut cache = c.borrow_mut();
        cache.get(&key).copied()
    })
}

/// Insert a value into the thread-local cache.
pub fn cache_insert(
    backend_id: usize,
    fluid: &FluidId,
    property: Property,
    state: &FluidState,
    value: f64,
) {
    let key = CacheKey::new(backend_id, fluid, property, state);
    CACHE.with(|c| {
        let mut cache = c.borrow_mut();
        cache.put(key, value);
    });
}

/// Clear the thread-local cache (e.g. at solver iteration boundaries).
pub fn cache_clear() {
    CACHE.with(|c| {
        let mut cache = c.borrow_mut();
        cache.clear();
    });
}

/// Resize the thread-local cache capacity.
pub fn cache_resize(capacity: NonZeroUsize) {
    CACHE.with(|c| {
        let mut cache = c.borrow_mut();
        cache.resize(capacity);
    });
}

#[cfg(test)]
mod tests {
    use super::*;
    use entropyk_core::{Pressure, Temperature};

    #[test]
    fn test_cache_key_quantization() {
        let fluid = FluidId::new("R134a");
        let state = FluidState::from_pt(Pressure::from_bar(1.0), Temperature::from_celsius(25.0));
        let key1 = CacheKey::new(0, &fluid, Property::Density, &state);
        let key2 = CacheKey::new(0, &fluid, Property::Density, &state);
        assert_eq!(key1, key2);
        // Equal keys must have same hash (for HashMap use)
        use std::collections::hash_map::DefaultHasher;
        let mut h1 = DefaultHasher::new();
        let mut h2 = DefaultHasher::new();
        key1.hash(&mut h1);
        key2.hash(&mut h2);
        assert_eq!(h1.finish(), h2.finish());
    }

    #[test]
    fn test_cache_key_different_states() {
        let fluid = FluidId::new("R134a");
        let state1 = FluidState::from_pt(Pressure::from_bar(1.0), Temperature::from_celsius(25.0));
        let state2 = FluidState::from_pt(Pressure::from_bar(2.0), Temperature::from_celsius(25.0));
        let key1 = CacheKey::new(0, &fluid, Property::Density, &state1);
        let key2 = CacheKey::new(0, &fluid, Property::Density, &state2);
        assert_ne!(key1, key2);
    }

    #[test]
    fn test_lru_eviction() {
        use std::num::NonZeroUsize;

        cache_clear();
        cache_resize(NonZeroUsize::new(2).expect("2 is non-zero"));

        let fluid = FluidId::new("R134a");
        let state1 = FluidState::from_pt(Pressure::from_bar(1.0), Temperature::from_celsius(20.0));
        let state2 = FluidState::from_pt(Pressure::from_bar(1.0), Temperature::from_celsius(25.0));
        let state3 = FluidState::from_pt(Pressure::from_bar(1.0), Temperature::from_celsius(30.0));

        cache_insert(0, &fluid, Property::Density, &state1, 1000.0);
        cache_insert(0, &fluid, Property::Density, &state2, 1100.0);
        cache_insert(0, &fluid, Property::Density, &state3, 1200.0);

        assert!(cache_get(0, &fluid, Property::Density, &state1).is_none());
        assert_eq!(cache_get(0, &fluid, Property::Density, &state2), Some(1100.0));
        assert_eq!(cache_get(0, &fluid, Property::Density, &state3), Some(1200.0));

        cache_resize(NonZeroUsize::new(DEFAULT_CACHE_CAPACITY).expect("capacity is non-zero"));
    }

    #[test]
    fn test_cache_key_different_backends() {
        let fluid = FluidId::new("R134a");
        let state = FluidState::from_pt(Pressure::from_bar(1.0), Temperature::from_celsius(25.0));
        let key1 = CacheKey::new(0, &fluid, Property::Density, &state);
        let key2 = CacheKey::new(1, &fluid, Property::Density, &state);
        assert_ne!(key1, key2);
    }
}
