use std::collections::HashMap;

/// An interned string identifier. Copy-cheap, O(1) hash and equality.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct Symbol(u64);

/// Bidirectional string ↔ Symbol mapping.
#[derive(Debug, Clone, Default)]
pub struct SymbolTable {
    to_id: HashMap<String, Symbol>,
    to_str: Vec<String>,
}

impl SymbolTable {
    #[allow(dead_code)]
    pub fn new() -> Self {
        Self::default()
    }

    /// Intern a string, returning its Symbol. Returns existing Symbol if
    /// already interned.
    pub fn intern(&mut self, s: &str) -> Symbol {
        if let Some(&id) = self.to_id.get(s) {
            return id;
        }
        let id = Symbol(self.to_str.len() as u64);
        self.to_str.push(s.to_string());
        self.to_id.insert(s.to_string(), id);
        id
    }

    /// Look up the Symbol for a string without interning.
    pub fn get(&self, s: &str) -> Option<Symbol> {
        self.to_id.get(s).copied()
    }

    /// Resolve a Symbol back to its string.
    pub fn resolve(&self, sym: Symbol) -> &str {
        &self.to_str[sym.0 as usize]
    }

    /// Intern a (category, item) pair.
    pub fn intern_pair(&mut self, cat: &str, item: &str) -> (Symbol, Symbol) {
        (self.intern(cat), self.intern(item))
    }

    /// Intern a full coordinate list.
    pub fn intern_coords(&mut self, coords: &[(String, String)]) -> Vec<(Symbol, Symbol)> {
        coords.iter().map(|(c, i)| self.intern_pair(c, i)).collect()
    }
}

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

    #[test]
    fn intern_returns_same_id() {
        let mut t = SymbolTable::new();
        let a = t.intern("hello");
        let b = t.intern("hello");
        assert_eq!(a, b);
    }

    #[test]
    fn different_strings_different_ids() {
        let mut t = SymbolTable::new();
        let a = t.intern("hello");
        let b = t.intern("world");
        assert_ne!(a, b);
    }

    #[test]
    fn resolve_roundtrips() {
        let mut t = SymbolTable::new();
        let s = t.intern("test");
        assert_eq!(t.resolve(s), "test");
    }
}
