use std::{
    borrow::Borrow,
    collections::HashMap,
    hash::Hash,
    time::{Duration, Instant},
};

pub(crate) struct ExpiringMap<K, V>
where
    K: Hash + Eq,
{
    data: HashMap<K, Value<V>>,
    duration: Duration,
}

impl<K: Hash + Eq, V> ExpiringMap<K, V> {
    pub fn new(duration: Duration) -> Self {
        Self {
            data: HashMap::new(),
            duration,
        }
    }

    pub fn clear(&mut self) {
        self.data.clear();
    }

    fn cleanup(&mut self) {
        let now = Instant::now();
        self.data.retain(|_, v| v.expiration > now);
        self.data.shrink_to_fit();
    }

    pub fn insert(&mut self, key: K, value: V) -> Option<V> {
        self.cleanup();
        self.data
            .insert(key, Value::new(value, Instant::now() + self.duration))
            .map(|v| v.value)
    }

    pub fn get<Q>(&self, key: &Q) -> Option<&V>
    where
        K: Borrow<Q>,
        Q: ?Sized + Ord + Hash,
    {
        self.data
            .get(key)
            .filter(|v| !v.is_expired())
            .map(Value::value)
    }

    pub fn contains_key<Q>(&self, key: &Q) -> bool
    where
        K: Borrow<Q>,
        Q: ?Sized + Ord + Hash,
    {
        self.get(key).is_some()
    }
}

struct Value<T> {
    value: T,
    expiration: Instant,
}

impl<T> Value<T> {
    fn new(value: T, expiration: Instant) -> Self {
        Self { value, expiration }
    }

    fn value(&self) -> &T {
        &self.value
    }

    fn is_expired(&self) -> bool {
        self.expiration < Instant::now()
    }
}

#[cfg(test)]
mod tests {
    use super::ExpiringMap;
    use std::{thread::sleep, time::Duration};

    #[test]
    fn test_insert() {
        let mut cache = ExpiringMap::new(Duration::from_millis(50));
        cache.insert("key", "value");
        assert_eq!(cache.get("key"), Some(&"value"));
        sleep(Duration::from_millis(60));
        assert!(cache.get("key").is_none());
        cache.cleanup();
        cache.insert("a", "b");
        assert_eq!(cache.data.len(), 1);
    }
}
