//! String interner for memory-efficient identifier storage.
//!
//! The interner deduplicates strings by storing each unique string only once,
//! returning `Arc<str>` handles that share the same underlying allocation.
//!
//! This implementation is thread-safe and can be shared across threads via `Arc<Interner>`.

use std::collections::HashSet;
use std::hash::{Hash, Hasher};
use std::sync::{Arc, RwLock};

/// Wrapper around Arc<str> that implements Hash/Eq based on string content.
/// This allows us to look up by &str in a HashSet<InternedString>.
#[derive(Debug, Clone)]
struct InternedString(Arc<str>);

impl Hash for InternedString {
    fn hash<H: Hasher>(&self, state: &mut H) {
        // Hash the string content, not the pointer
        self.0.as_ref().hash(state);
    }
}

impl PartialEq for InternedString {
    fn eq(&self, other: &Self) -> bool {
        // Compare string content, not pointers
        self.0.as_ref() == other.0.as_ref()
    }
}

impl Eq for InternedString {}

impl std::borrow::Borrow<str> for InternedString {
    fn borrow(&self) -> &str {
        self.0.as_ref()
    }
}

/// A thread-safe string interner that deduplicates strings for memory efficiency.
///
/// Each unique string is stored once, and subsequent calls to `intern()`
/// with the same string return a clone of the existing `Arc<str>`.
#[derive(Debug, Default)]
pub struct Interner {
    /// Set of interned strings. We use a wrapper type that implements
    /// Borrow<str> so we can look up by &str without allocating.
    strings: RwLock<HashSet<InternedString>>,
}

impl Interner {
    /// Create a new empty interner.
    pub fn new() -> Self {
        Self {
            strings: RwLock::new(HashSet::new()),
        }
    }

    /// Intern a string, returning a shared reference.
    ///
    /// If the string has been interned before, returns a clone of the
    /// existing `Arc<str>`. Otherwise, allocates a new `Arc<str>` and
    /// stores it for future lookups.
    pub fn intern(&self, s: &str) -> Arc<str> {
        // Fast path: check if already interned with read lock
        {
            let strings = self.strings.read().unwrap();
            if let Some(existing) = strings.get(s) {
                return existing.0.clone();
            }
        }

        // Slow path: acquire write lock and insert
        let mut strings = self.strings.write().unwrap();

        // Double-check after acquiring write lock (another thread may have inserted)
        if let Some(existing) = strings.get(s) {
            return existing.0.clone();
        }

        // Not found - allocate once and store
        let arc: Arc<str> = Arc::from(s);
        strings.insert(InternedString(arc.clone()));
        arc
    }

    /// Returns the number of unique strings interned.
    pub fn len(&self) -> usize {
        self.strings.read().unwrap().len()
    }

    /// Returns true if no strings have been interned.
    pub fn is_empty(&self) -> bool {
        self.strings.read().unwrap().is_empty()
    }
}

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

    #[test]
    fn test_intern_returns_same_arc() {
        let interner = Interner::new();

        let a1 = interner.intern("hello");
        let a2 = interner.intern("hello");

        // Should be the same Arc (pointer equality)
        assert!(Arc::ptr_eq(&a1, &a2));
    }

    #[test]
    fn test_intern_different_strings() {
        let interner = Interner::new();

        let a = interner.intern("hello");
        let b = interner.intern("world");

        // Different strings should have different Arcs
        assert!(!Arc::ptr_eq(&a, &b));
        assert_eq!(&*a, "hello");
        assert_eq!(&*b, "world");
    }

    #[test]
    fn test_intern_count() {
        let interner = Interner::new();

        assert_eq!(interner.len(), 0);
        assert!(interner.is_empty());

        interner.intern("a");
        assert_eq!(interner.len(), 1);

        interner.intern("b");
        assert_eq!(interner.len(), 2);

        // Interning same string again doesn't increase count
        interner.intern("a");
        assert_eq!(interner.len(), 2);
    }
}
