An implementation of git fast-export for Pijul
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::sync::{Arc, RwLock};

use libpijul::change::{Change, ChangeError, ChangeHeader};
use libpijul::changestore::ChangeStore;
use libpijul::pristine::{Hash, Position};
use libpijul::{Base32, ChangeId, Merkle, Vertex};

use thiserror::Error;

#[derive(Clone)]
/// A change store that eagerly decompresses the content of changes,
/// and keeps them in memory.
pub struct EagerChangeStore {
    changes: Arc<RwLock<HashMap<Hash, Change>>>,
    tags: Arc<RwLock<HashMap<Merkle, ChangeHeader>>>,
    changes_dir: PathBuf,
}

#[derive(Debug, Error)]
pub enum Error {
    #[error(transparent)]
    Io(#[from] std::io::Error),
    #[error(transparent)]
    Utf8(#[from] std::str::Utf8Error),
    #[error(transparent)]
    Change(#[from] libpijul::change::ChangeError),
    #[error(transparent)]
    Bincode(#[from] bincode::Error),
    #[error(transparent)]
    Tag(#[from] libpijul::tag::TagError),
    #[error(transparent)]
    Persist(#[from] tempfile::PersistError),
}

impl EagerChangeStore {
    pub fn from_root<P: AsRef<Path>>(root: P) -> Self {
        EagerChangeStore {
            changes: Arc::default(),
            tags: Arc::default(),
            changes_dir: root.as_ref().join(".pijul").join("changes"),
        }
    }

    fn load(&self, h: &Hash) -> Result<(), Error> {
        {
            let r = self.changes.read().unwrap();
            if let Some(_) = r.get(h) {
                return Ok(());
            }
        }

        let path = self.filename(h);
        let file_name = path.to_str().unwrap();
        let p = Change::deserialize(&file_name, Some(h))?;
        self.changes.write().unwrap().insert(*h, p);
        Ok(())
    }

    fn filename(&self, h: &Hash) -> PathBuf {
        let mut path = self.changes_dir.clone();
        let b32 = h.to_base32();
        let (a, b) = b32.split_at(2);
        path.push(a);
        path.push(b);
        path.set_extension("change");
        path
    }
}

impl ChangeStore for EagerChangeStore {
    type Error = Error;

    fn get_change(&self, h: &Hash) -> Result<Change, Self::Error> {
        self.load(h)?;
        let r = self.changes.read().unwrap();
        Ok(r.get(h).unwrap().clone())
    }

    fn get_tag_header(&self, h: &Merkle) -> Result<ChangeHeader, Self::Error> {
        let r = self.tags.read().unwrap();
        if let Some(t) = r.get(&h) {
            Ok(t.clone())
        } else {
            let mut path = self.changes_dir.clone();
            let b32 = h.to_base32();
            let (a, b) = b32.split_at(2);
            path.push(a);
            path.push(b);
            path.set_extension("tag");
            let mut p = libpijul::tag::OpenTagFile::open(&path, h)?;
            let header = p.header()?;
            self.tags.write().unwrap().insert(*h, header.clone());
            Ok(header)
        }
    }

    fn has_contents(&self, hash: Hash, _: Option<ChangeId>) -> bool {
        match self.load(&hash) {
            Ok(_) => {
                let r = self.changes.read().unwrap();
                match r.get(&hash) {
                    Some(c) => !c.contents.is_empty(),
                    None => false,
                }
            }
            Err(_) => false,
        }
    }

    fn get_contents<F: Fn(ChangeId) -> Option<Hash>>(
        &self,
        hash: F,
        key: Vertex<ChangeId>,
        buf: &mut [u8],
    ) -> Result<usize, Self::Error> {
        if key.end <= key.start {
            return Ok(0);
        }
        assert_eq!(buf.len(), key.end - key.start);
        let h = hash(key.change).unwrap();
        self.load(&h)?;
        let r = self.changes.read().unwrap();
        let p = r.get(&h).unwrap();
        let start: usize = key.start.0 .0.try_into().unwrap();
        let end: usize = key.end.0 .0.try_into().unwrap();
        buf.clone_from_slice(&p.contents[start..end]);
        Ok(end - start)
    }

    fn get_contents_ext(
        &self,
        key: Vertex<Option<Hash>>,
        buf: &mut [u8],
    ) -> Result<usize, Self::Error> {
        if let Some(change) = key.change {
            if key.end <= key.start {
                return Ok(0);
            }
            assert_eq!(key.end - key.start, buf.len());
            self.load(&change)?;
            let r = self.changes.read().unwrap();
            let p = r.get(&change).unwrap();
            let start: usize = key.start.0 .0.try_into().unwrap();
            let end: usize = key.end.0 .0.try_into().unwrap();
            buf.clone_from_slice(&p.contents[start..end]);
            Ok(end - start)
        } else {
            Ok(0)
        }
    }

    fn change_deletes_position<F: Fn(ChangeId) -> Option<Hash>>(
        &self,
        hash: F,
        change: ChangeId,
        pos: Position<Option<Hash>>,
    ) -> Result<Vec<Hash>, Self::Error> {
        let r = self.changes.read().unwrap();
        let change = r.get(&hash(change).unwrap()).unwrap();
        let mut v = Vec::new();
        for c in change.changes.iter() {
            for c in c.iter() {
                v.extend(c.deletes_pos(pos).into_iter())
            }
        }
        Ok(v)
    }

    fn save_change<
        E: From<Self::Error> + From<ChangeError>,
        F: FnOnce(&mut Change, &Hash) -> Result<(), E>,
    >(
        &self,
        p: &mut Change,
        ff: F,
    ) -> Result<Hash, E> {
        let mut f = match tempfile::NamedTempFile::new_in(&self.changes_dir) {
            Ok(f) => f,
            Err(e) => return Err(E::from(Error::from(e))),
        };
        let hash = {
            let w = std::io::BufWriter::new(&mut f);
            p.serialize(w, ff)?
        };
        let file_name = self.filename(&hash);
        if let Err(e) = std::fs::create_dir_all(file_name.parent().unwrap()) {
            return Err(E::from(Error::from(e)));
        }
        if let Err(e) = f.persist(file_name) {
            return Err(E::from(Error::from(e)));
        }

        let mut w = self.changes.write().unwrap();
        w.insert(hash, p.clone());
        Ok(hash)
    }

    fn del_change(&self, h: &Hash) -> Result<bool, Self::Error> {
        let file_name = self.filename(h);
        std::fs::remove_file(&file_name).unwrap_or(());
        std::fs::remove_dir(file_name.parent().unwrap()).unwrap_or(()); // fails silently if there are still changes with the same 2-letter prefix.
        let mut w = self.changes.write().unwrap();
        Ok(w.remove(h).is_some())
    }
}