use std::{
    fmt::{self, Debug},
    ops::ControlFlow,
};

use dashmap::DashMap;
use rayon::iter::{IntoParallelRefIterator, ParallelIterator};
use tracing::{info_span, instrument};

use super::{Content, Dir, Query, VPath, VPathBuf, query, walk_dir_rec};

pub struct ContentCache<T> {
    inner: T,
    cache: DashMap<VPathBuf, Content>,
}

impl<T> ContentCache<T> {
    pub fn new(inner: T) -> Self {
        Self {
            inner,
            cache: DashMap::new(),
        }
    }
}

impl<T> ContentCache<T>
where
    T: Dir + Send + Sync,
{
    #[instrument(name = "ContentCache::warm_up", skip(self))]
    pub fn warm_up(&self) {
        let mut paths = vec![];
        walk_dir_rec(&self.inner, VPath::ROOT, &mut |path| {
            paths.push(path.to_owned());
            ControlFlow::Continue(())
        });

        paths.par_iter().for_each(|path| _ = self.content(path));
    }
}

impl<T> ContentCache<T>
where
    T: Dir,
{
    #[instrument(name = "ContentCache::content", skip(self))]
    fn content(&self, path: &VPath) -> Option<Content> {
        self.cache.get(path).map(|x| x.clone()).or_else(|| {
            let _span = info_span!("cache_miss").entered();

            let content = query::<Content>(&self.inner, path);
            if let Some(content) = &content {
                self.cache.insert(path.to_owned(), content.clone());
            }
            content
        })
    }
}

impl<T> Dir for ContentCache<T>
where
    T: Dir,
{
    fn query(&self, path: &VPath, query: &mut Query) {
        query.try_provide(|| self.content(path));

        self.inner.query(path, query);
    }
}

impl<T> fmt::Debug for ContentCache<T>
where
    T: Debug,
{
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "ContentCache({:?})", self.inner)
    }
}
