use crate::pack::{self, index};
use git_features::progress::{self, Progress};
use git_hash::SIZE_OF_SHA1_DIGEST as SHA1_SIZE;
use git_object::{
    borrowed,
    bstr::{BString, ByteSlice},
    owned, HashKind,
};
#[derive(thiserror::Error, Debug)]
#[allow(missing_docs)]
pub enum Error {
    #[error("index checksum mismatch: expected {expected}, got {actual}")]
    Mismatch { expected: owned::Id, actual: owned::Id },
    #[error("{kind} object {id} could not be decoded")]
    ObjectDecode {
        source: borrowed::Error,
        kind: git_object::Kind,
        id: owned::Id,
    },
    #[error("{kind} object {id} wasn't re-encoded without change, wanted\n{expected}\n\nGOT\n\n{actual}")]
    ObjectEncodeMismatch {
        kind: git_object::Kind,
        id: owned::Id,
        expected: BString,
        actual: BString,
    },
    #[error(transparent)]
    ObjectEncode(#[from] std::io::Error),
}
#[derive(Debug, Eq, PartialEq, Hash, Clone, Copy)]
pub enum Mode {
    
    Sha1CRC32,
    
    
    Sha1CRC32Decode,
    
    
    Sha1CRC32DecodeEncode,
}
impl index::File {
    
    
    
    pub fn index_checksum(&self) -> owned::Id {
        owned::Id::from_20_bytes(&self.data[self.data.len() - SHA1_SIZE..])
    }
    
    
    
    pub fn pack_checksum(&self) -> owned::Id {
        let from = self.data.len() - SHA1_SIZE * 2;
        owned::Id::from_20_bytes(&self.data[from..from + SHA1_SIZE])
    }
    
    
    pub fn verify_checksum(&self, mut progress: impl Progress) -> Result<owned::Id, Error> {
        let data_len_without_trailer = self.data.len() - SHA1_SIZE;
        let actual = match git_features::hash::bytes_of_file(
            &self.path,
            data_len_without_trailer,
            HashKind::Sha1,
            &mut progress,
        ) {
            Ok(id) => id,
            Err(_io_err) => {
                let start = std::time::Instant::now();
                let mut hasher = git_features::hash::Sha1::default();
                hasher.update(&self.data[..data_len_without_trailer]);
                progress.inc_by(data_len_without_trailer);
                progress.show_throughput(start);
                owned::Id::new_sha1(hasher.digest())
            }
        };
        let expected = self.index_checksum();
        if actual == expected {
            Ok(actual)
        } else {
            Err(Error::Mismatch { actual, expected })
        }
    }
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    pub fn verify_integrity<C, P>(
        &self,
        pack: Option<(
            &pack::data::File,
            Mode,
            index::traverse::Algorithm,
            impl Fn() -> C + Send + Sync,
        )>,
        thread_limit: Option<usize>,
        progress: Option<P>,
    ) -> Result<
        (owned::Id, Option<index::traverse::Outcome>, Option<P>),
        index::traverse::Error<pack::index::verify::Error>,
    >
    where
        P: Progress,
        C: pack::cache::DecodeEntry,
    {
        let mut root = progress::DoOrDiscard::from(progress);
        match pack {
            Some((pack, mode, algorithm, make_cache)) => self
                .traverse(
                    pack,
                    root.into_inner(),
                    || {
                        let mut encode_buf = Vec::with_capacity(2048);
                        move |kind, data, index_entry, progress| {
                            Self::verify_entry(mode, &mut encode_buf, kind, data, index_entry, progress)
                        }
                    },
                    make_cache,
                    index::traverse::Options {
                        algorithm,
                        thread_limit,
                        check: index::traverse::SafetyCheck::All,
                    },
                )
                .map(|(id, outcome, root)| (id, Some(outcome), root)),
            None => self
                .verify_checksum(root.add_child("Sha1 of index"))
                .map_err(Into::into)
                .map(|id| (id, None, root.into_inner())),
        }
    }
    #[allow(clippy::too_many_arguments)]
    pub(crate) fn verify_entry<P>(
        mode: Mode,
        encode_buf: &mut Vec<u8>,
        object_kind: git_object::Kind,
        buf: &[u8],
        index_entry: &index::Entry,
        progress: &mut P,
    ) -> Result<(), Error>
    where
        P: Progress,
    {
        if let Mode::Sha1CRC32Decode | Mode::Sha1CRC32DecodeEncode = mode {
            use git_object::Kind::*;
            match object_kind {
                Tree | Commit | Tag => {
                    let borrowed_object =
                        borrowed::Object::from_bytes(object_kind, buf).map_err(|err| Error::ObjectDecode {
                            source: err,
                            kind: object_kind,
                            id: index_entry.oid,
                        })?;
                    if let Mode::Sha1CRC32DecodeEncode = mode {
                        let object = owned::Object::from(borrowed_object);
                        encode_buf.clear();
                        object.write_to(&mut *encode_buf)?;
                        if encode_buf.as_slice() != buf {
                            let mut should_return_error = true;
                            if let git_object::Kind::Tree = object_kind {
                                if buf.as_bstr().find(b"100664").is_some() || buf.as_bstr().find(b"100640").is_some() {
                                    progress.info(format!("Tree object {} would be cleaned up during re-serialization, replacing mode '100664|100640' with '100644'", index_entry.oid));
                                    should_return_error = false
                                }
                            }
                            if should_return_error {
                                return Err(Error::ObjectEncodeMismatch {
                                    kind: object_kind,
                                    id: index_entry.oid,
                                    expected: buf.into(),
                                    actual: encode_buf.clone().into(),
                                });
                            }
                        }
                    }
                }
                Blob => {}
            };
        }
        Ok(())
    }
}