🏡 index : ~doyle/rgit.git

use std::collections::BTreeMap;

use anyhow::Context;
use gix::{bstr::BStr, ObjectId};
use itertools::{Either, Itertools};
use rkyv::{Archive, Serialize};
use rocksdb::{WriteBatch, DB};
use yoke::{Yoke, Yokeable};

use super::{
    prefixes::{TREE_FAMILY, TREE_ITEM_FAMILY},
    Yoked,
};

#[derive(Serialize, Archive, Debug, PartialEq, Eq, Hash)]
pub struct Tree {
    pub indexed_tree_id: u64,
}

impl Tree {
    pub fn insert(
        &self,
        database: &DB,
        batch: &mut WriteBatch,
        tree_oid: ObjectId,
    ) -> Result<(), anyhow::Error> {
        let cf = database
            .cf_handle(TREE_FAMILY)
            .context("tree column family missing")?;

        batch.put_cf(
            cf,
            tree_oid.as_slice(),
            rkyv::to_bytes::<rkyv::rancor::Error>(self)?,
        );

        Ok(())
    }

    pub fn find(database: &DB, tree_oid: ObjectId) -> Result<Option<u64>, anyhow::Error> {
        let cf = database
            .cf_handle(TREE_FAMILY)
            .context("tree column family missing")?;

        let Some(data) = database.get_pinned_cf(cf, tree_oid.as_slice())? else {
            return Ok(None);
        };

        let data = rkyv::access::<<Self as Archive>::Archived, rkyv::rancor::Error>(data.as_ref())?;

        Ok(Some(data.indexed_tree_id.to_native()))
    }
}

#[derive(Serialize, Archive, Debug, PartialEq, Eq, Hash, Ord, PartialOrd)]
#[rkyv(derive(Ord, PartialOrd, Eq, PartialEq, Debug))]
#[rkyv(compare(PartialOrd, PartialEq))]
pub struct TreeKey(pub String);

#[derive(Serialize, Archive, Debug, PartialEq, Eq, Default, Yokeable)]
pub struct SortedTree(pub BTreeMap<TreeKey, SortedTreeItem>);

impl SortedTree {
    pub fn insert(
        &self,
        digest: u64,
        database: &DB,
        batch: &mut WriteBatch,
    ) -> Result<(), anyhow::Error> {
        let cf = database
            .cf_handle(TREE_ITEM_FAMILY)
            .context("tree column family missing")?;

        batch.put_cf(
            cf,
            digest.to_ne_bytes(),
            rkyv::to_bytes::<rkyv::rancor::Error>(self)?,
        );

        Ok(())
    }

    pub fn get(digest: u64, database: &DB) -> Result<Option<YokedSortedTree>, anyhow::Error> {
        let cf = database
            .cf_handle(TREE_ITEM_FAMILY)
            .expect("tree column family missing");

        database
            .get_cf(cf, digest.to_ne_bytes())?
            .map(|data| {
                Yoke::try_attach_to_cart(data.into_boxed_slice(), |data| {
                    rkyv::access::<_, rkyv::rancor::Error>(data)
                })
            })
            .transpose()
            .context("failed to parse full tree")
    }
}

#[derive(Serialize, Archive, Debug, PartialEq, Eq)]
#[rkyv(
    bytecheck(bounds(__C: rkyv::validation::ArchiveContext)),
    serialize_bounds(__S: rkyv::ser::Writer + rkyv::ser::Allocator, __S::Error: rkyv::rancor::Source),
)]
pub enum SortedTreeItem {
    File,
    Directory(#[rkyv(omit_bounds)] SortedTree),
}

#[derive(Serialize, Archive, Debug, PartialEq, Eq, Hash)]
pub struct Submodule {
    pub url: String,
    pub oid: [u8; 20],
}

#[derive(Serialize, Archive, Debug, PartialEq, Eq, Hash)]
pub enum TreeItemKind {
    Submodule(Submodule),
    Tree,
    File,
}

#[derive(Serialize, Archive, Debug, PartialEq, Eq, Hash, Yokeable)]
pub struct TreeItem {
    pub mode: u16,
    pub kind: TreeItemKind,
}

pub type YokedSortedTree = Yoked<&'static <SortedTree as Archive>::Archived>;
pub type YokedTreeItem = Yoked<&'static <TreeItem as Archive>::Archived>;
pub type YokedTreeItemKey = Yoked<&'static [u8]>;
pub type YokedTreeItemKeyUtf8 = Yoked<&'static str>;

impl TreeItem {
    pub fn insert(
        &self,
        buffer: &mut Vec<u8>,
        digest: u64,
        path: &BStr,
        database: &DB,
        batch: &mut WriteBatch,
    ) -> Result<(), anyhow::Error> {
        let cf = database
            .cf_handle(TREE_ITEM_FAMILY)
            .context("tree column family missing")?;

        buffer.clear();
        buffer.reserve(std::mem::size_of::<u64>() + path.len() + std::mem::size_of::<usize>());
        buffer.extend_from_slice(&digest.to_ne_bytes());
        buffer.extend_from_slice(&memchr::memchr_iter(b'/', path).count().to_be_bytes());
        buffer.extend_from_slice(path.as_ref());

        batch.put_cf(cf, &buffer, rkyv::to_bytes::<rkyv::rancor::Error>(self)?);

        Ok(())
    }

    pub fn find_exact(
        database: &DB,
        digest: u64,
        path: &[u8],
    ) -> Result<Option<YokedTreeItem>, anyhow::Error> {
        let cf = database
            .cf_handle(TREE_ITEM_FAMILY)
            .expect("tree column family missing");

        let mut buffer = Vec::with_capacity(std::mem::size_of::<u64>() + path.len());
        buffer.extend_from_slice(&digest.to_ne_bytes());
        buffer.extend_from_slice(&memchr::memchr_iter(b'/', path).count().to_be_bytes());
        buffer.extend_from_slice(path);

        database
            .get_cf(cf, buffer)?
            .map(|data| {
                Yoke::try_attach_to_cart(data.into_boxed_slice(), |data| {
                    rkyv::access::<_, rkyv::rancor::Error>(data)
                })
            })
            .transpose()
            .context("failed to parse tree item")
    }

    pub fn find_prefix<'a>(
        database: &'a DB,
        digest: u64,
        prefix: Option<&[u8]>,
    ) -> impl Iterator<Item = Result<(YokedTreeItemKey, YokedTreeItem), anyhow::Error>> + use<'a>
    {
        let cf = database
            .cf_handle(TREE_ITEM_FAMILY)
            .expect("tree column family missing");

        let (iterator, key) = match prefix {
            None => {
                let iterator = database.prefix_iterator_cf(cf, digest.to_ne_bytes());

                (iterator, Either::Left(Either::Left(digest.to_be_bytes())))
            }
            Some([]) => {
                let mut buffer = [0_u8; std::mem::size_of::<u64>() + std::mem::size_of::<usize>()];
                buffer[..std::mem::size_of::<u64>()].copy_from_slice(&digest.to_ne_bytes());
                buffer[std::mem::size_of::<u64>()..].copy_from_slice(&0_usize.to_be_bytes());

                let iterator = database.prefix_iterator_cf(cf, buffer);

                (iterator, Either::Left(Either::Right(buffer)))
            }
            Some(prefix) => {
                let mut buffer = Vec::with_capacity(
                    std::mem::size_of::<u64>() + prefix.len() + std::mem::size_of::<usize>(),
                );
                buffer.extend_from_slice(&digest.to_ne_bytes());
                buffer.extend_from_slice(
                    &(memchr::memchr_iter(b'/', prefix).count() + 1).to_be_bytes(),
                );
                buffer.extend_from_slice(prefix);
                buffer.push(b'/');

                let iterator = database.prefix_iterator_cf(cf, &buffer);

                (iterator, Either::Right(buffer))
            }
        };

        iterator
            .take_while(move |v| {
                v.as_ref().is_ok_and(|(k, _)| {
                    k.starts_with(match key.as_ref() {
                        Either::Left(Either::Right(v)) => v.as_ref(),
                        Either::Left(Either::Left(v)) => v.as_ref(),
                        Either::Right(v) => v.as_ref(),
                    })
                })
            })
            .map_ok(|(key, value)| {
                let key = Yoke::attach_to_cart(key, |data| {
                    &data[std::mem::size_of::<u64>() + std::mem::size_of::<usize>()..]
                });
                let value = Yoke::try_attach_to_cart(value, |data| {
                    rkyv::access::<_, rkyv::rancor::Error>(data)
                })
                .context("Failed to open repository")?;
                Ok((key, value))
            })
            .flatten()
    }

    pub fn contains(database: &DB, digest: u64) -> Result<bool, anyhow::Error> {
        let cf = database
            .cf_handle(TREE_ITEM_FAMILY)
            .context("tree column family missing")?;

        Ok(database
            .prefix_iterator_cf(cf, digest.to_ne_bytes())
            .next()
            .transpose()?
            .is_some())
    }
}