🏡 index : ~doyle/rgit.git

use std::{collections::HashSet, sync::Arc};

use anyhow::Context;
use gix::actor::SignatureRef;
use rkyv::{Archive, Serialize};
use yoke::{Yoke, Yokeable};

use crate::database::schema::{
    commit::{ArchivedAuthor, Author},
    prefixes::TAG_FAMILY,
    repository::RepositoryId,
    Yoked,
};

#[derive(Serialize, Archive, Debug, Yokeable)]
pub struct Tag {
    pub tagger: Option<Author>,
}

impl Tag {
    pub fn new(tagger: Option<SignatureRef<'_>>) -> Result<Self, anyhow::Error> {
        Ok(Self {
            tagger: tagger.map(TryFrom::try_from).transpose()?,
        })
    }

    pub fn insert(&self, batch: &TagTree, name: &str) -> Result<(), anyhow::Error> {
        batch.insert(name, self)
    }
}

pub struct TagTree {
    db: Arc<rocksdb::DB>,
    prefix: RepositoryId,
}

pub type YokedString = Yoked<&'static str>;
pub type YokedTag = Yoked<&'static <Tag as Archive>::Archived>;

impl TagTree {
    pub(super) fn new(db: Arc<rocksdb::DB>, prefix: RepositoryId) -> Self {
        Self { db, prefix }
    }

    pub fn insert(&self, name: &str, value: &Tag) -> anyhow::Result<()> {
        let cf = self
            .db
            .cf_handle(TAG_FAMILY)
            .context("missing tag column family")?;

        let mut db_name = self.prefix.to_be_bytes().to_vec();
        db_name.extend_from_slice(name.as_ref());

        self.db
            .put_cf(cf, db_name, rkyv::to_bytes::<rkyv::rancor::Error>(value)?)?;

        Ok(())
    }

    pub fn remove(&self, name: &str) -> anyhow::Result<()> {
        let cf = self
            .db
            .cf_handle(TAG_FAMILY)
            .context("missing tag column family")?;

        let mut db_name = self.prefix.to_be_bytes().to_vec();
        db_name.extend_from_slice(name.as_ref());
        self.db.delete_cf(cf, db_name)?;

        Ok(())
    }

    pub fn list(&self) -> anyhow::Result<HashSet<String>> {
        let cf = self
            .db
            .cf_handle(TAG_FAMILY)
            .context("missing tag column family")?;

        Ok(self
            .db
            .prefix_iterator_cf(cf, self.prefix.to_be_bytes())
            .filter_map(Result::ok)
            .filter_map(|(k, _)| {
                Some(
                    String::from_utf8_lossy(k.strip_prefix(&self.prefix.to_be_bytes())?)
                        .to_string(),
                )
            })
            .collect())
    }

    pub fn fetch_all(&self) -> anyhow::Result<Vec<(YokedString, YokedTag)>> {
        let cf = self
            .db
            .cf_handle(TAG_FAMILY)
            .context("missing tag column family")?;

        let mut res = self
            .db
            .prefix_iterator_cf(cf, self.prefix.to_be_bytes())
            .filter_map(Result::ok)
            .filter_map(|(name, value)| {
                let name = Yoke::try_attach_to_cart(name, |data| {
                    let data = data
                        .strip_prefix(&self.prefix.to_be_bytes())
                        .ok_or(())?
                        .strip_prefix(b"refs/tags/")
                        .ok_or(())?;
                    simdutf8::basic::from_utf8(data).map_err(|_| ())
                })
                .ok()?;

                Some((name, value))
            })
            .map(|(name, value)| {
                let value = Yoke::try_attach_to_cart(value, |data| {
                    rkyv::access::<_, rkyv::rancor::Error>(data)
                })?;
                Ok((name, value))
            })
            .collect::<anyhow::Result<Vec<(YokedString, YokedTag)>>>()?;

        res.sort_unstable_by(|a, b| {
            let a_tagger = a.1.get().tagger.as_ref().map(ArchivedAuthor::time);
            let b_tagger = b.1.get().tagger.as_ref().map(ArchivedAuthor::time);
            b_tagger.cmp(&a_tagger)
        });

        Ok(res)
    }
}