use std::{
    cell::RefCell,
    ops::{Deref, DerefMut},
    rc::Rc,
    sync::Arc,
};
use crate::Cache;
pub type PackCache = dyn gix_pack::cache::DecodeEntry + Send + 'static;
pub type NewPackCacheFn = dyn Fn() -> Box<PackCache> + Send + Sync + 'static;
pub type ObjectCache = dyn gix_pack::cache::Object + Send + 'static;
pub type NewObjectCacheFn = dyn Fn() -> Box<ObjectCache> + Send + Sync + 'static;
impl Cache<crate::store::Handle<Rc<crate::Store>>> {
    pub fn into_arc(self) -> std::io::Result<Cache<crate::store::Handle<Arc<crate::Store>>>> {
        let inner = self.inner.into_arc()?;
        Ok(Cache {
            inner,
            new_pack_cache: self.new_pack_cache,
            new_object_cache: self.new_object_cache,
            pack_cache: self.pack_cache,
            object_cache: self.object_cache,
        })
    }
}
impl Cache<crate::store::Handle<Arc<crate::Store>>> {
    pub fn into_arc(self) -> std::io::Result<Cache<crate::store::Handle<Arc<crate::Store>>>> {
        Ok(self)
    }
}
impl<S> Cache<S> {
    pub fn into_inner(self) -> S {
        self.inner
    }
    pub fn with_pack_cache(mut self, create: impl Fn() -> Box<PackCache> + Send + Sync + 'static) -> Self {
        self.pack_cache = Some(RefCell::new(create()));
        self.new_pack_cache = Some(Arc::new(create));
        self
    }
    pub fn with_object_cache(mut self, create: impl Fn() -> Box<ObjectCache> + Send + Sync + 'static) -> Self {
        self.object_cache = Some(RefCell::new(create()));
        self.new_object_cache = Some(Arc::new(create));
        self
    }
    pub fn set_pack_cache(&mut self, create: impl Fn() -> Box<PackCache> + Send + Sync + 'static) {
        self.pack_cache = Some(RefCell::new(create()));
        self.new_pack_cache = Some(Arc::new(create));
    }
    pub fn set_object_cache(&mut self, create: impl Fn() -> Box<ObjectCache> + Send + Sync + 'static) {
        self.object_cache = Some(RefCell::new(create()));
        self.new_object_cache = Some(Arc::new(create));
    }
    pub fn has_object_cache(&self) -> bool {
        self.object_cache.is_some()
    }
    pub fn has_pack_cache(&self) -> bool {
        self.pack_cache.is_some()
    }
    pub fn unset_pack_cache(&mut self) {
        self.pack_cache = None;
        self.new_pack_cache = None;
    }
    pub fn unset_object_cache(&mut self) {
        self.object_cache = None;
        self.new_object_cache = None;
    }
}
impl<S> From<S> for Cache<S>
where
    S: gix_pack::Find,
{
    fn from(store: S) -> Self {
        Self {
            inner: store,
            pack_cache: None,
            new_pack_cache: None,
            object_cache: None,
            new_object_cache: None,
        }
    }
}
impl<S: Clone> Clone for Cache<S> {
    fn clone(&self) -> Self {
        Cache {
            inner: self.inner.clone(),
            new_pack_cache: self.new_pack_cache.clone(),
            new_object_cache: self.new_object_cache.clone(),
            pack_cache: self.new_pack_cache.as_ref().map(|create| RefCell::new(create())),
            object_cache: self.new_object_cache.as_ref().map(|create| RefCell::new(create())),
        }
    }
}
impl<S> Deref for Cache<S> {
    type Target = S;
    fn deref(&self) -> &Self::Target {
        &self.inner
    }
}
impl<S> DerefMut for Cache<S> {
    fn deref_mut(&mut self) -> &mut Self::Target {
        &mut self.inner
    }
}
mod impls {
    use std::{cell::RefCell, io::Read, ops::DerefMut};
    use gix_hash::{oid, ObjectId};
    use gix_object::{Data, Kind};
    use gix_pack::cache::Object;
    use crate::{find::Header, pack::data::entry::Location, Cache};
    impl<S> crate::Write for Cache<S>
    where
        S: crate::Write,
    {
        fn write_stream(&self, kind: Kind, size: u64, from: &mut dyn Read) -> Result<ObjectId, crate::write::Error> {
            self.inner.write_stream(kind, size, from)
        }
    }
    impl<S> crate::Find for Cache<S>
    where
        S: gix_pack::Find,
    {
        fn contains(&self, id: &oid) -> bool {
            self.inner.contains(id)
        }
        fn try_find<'a>(&self, id: &oid, buffer: &'a mut Vec<u8>) -> Result<Option<Data<'a>>, crate::find::Error> {
            gix_pack::Find::try_find(self, id, buffer).map(|t| t.map(|t| t.0))
        }
    }
    impl<S> crate::Header for Cache<S>
    where
        S: crate::Header,
    {
        fn try_header(&self, id: &oid) -> Result<Option<Header>, crate::find::Error> {
            self.inner.try_header(id)
        }
    }
    impl<S> gix_pack::Find for Cache<S>
    where
        S: gix_pack::Find,
    {
        fn contains(&self, id: &oid) -> bool {
            self.inner.contains(id)
        }
        fn try_find<'a>(
            &self,
            id: &oid,
            buffer: &'a mut Vec<u8>,
        ) -> Result<Option<(Data<'a>, Option<Location>)>, crate::find::Error> {
            match self.pack_cache.as_ref().map(RefCell::borrow_mut) {
                Some(mut pack_cache) => self.try_find_cached(id, buffer, pack_cache.deref_mut()),
                None => self.try_find_cached(id, buffer, &mut gix_pack::cache::Never),
            }
        }
        fn try_find_cached<'a>(
            &self,
            id: &oid,
            buffer: &'a mut Vec<u8>,
            pack_cache: &mut dyn gix_pack::cache::DecodeEntry,
        ) -> Result<Option<(Data<'a>, Option<gix_pack::data::entry::Location>)>, crate::find::Error> {
            if let Some(mut obj_cache) = self.object_cache.as_ref().map(RefCell::borrow_mut) {
                if let Some(kind) = obj_cache.get(&id.as_ref().to_owned(), buffer) {
                    return Ok(Some((Data::new(kind, buffer), None)));
                }
            }
            let possibly_obj = self.inner.try_find_cached(id.as_ref(), buffer, pack_cache)?;
            if let (Some(mut obj_cache), Some((obj, _location))) =
                (self.object_cache.as_ref().map(RefCell::borrow_mut), &possibly_obj)
            {
                obj_cache.put(id.as_ref().to_owned(), obj.kind, obj.data);
            }
            Ok(possibly_obj)
        }
        fn location_by_oid(&self, id: &oid, buf: &mut Vec<u8>) -> Option<gix_pack::data::entry::Location> {
            self.inner.location_by_oid(id, buf)
        }
        fn pack_offsets_and_oid(&self, pack_id: u32) -> Option<Vec<(u64, gix_hash::ObjectId)>> {
            self.inner.pack_offsets_and_oid(pack_id)
        }
        fn entry_by_location(&self, location: &Location) -> Option<gix_pack::find::Entry> {
            self.inner.entry_by_location(location)
        }
    }
}