use crate::{
    bare::GitBare,
    cmd::{
        oid::ObjectId,
        tree::{TreeEntry, TreeKind},
    },
    errors::{GitError, GitResult},
};

const LFS_POINTER_PREFIX: &[u8] = b"version https://git-lfs.github.com/spec/v";
const LFS_OID_MARKER: &[u8] = b"\noid sha256:";
const LFS_POINTER_MAX_SIZE: usize = 512;

impl GitBare {
    fn blob_is_lfs_pointer(&self, id: ObjectId) -> GitResult<bool> {
        let repo = self.gix_repo()?;
        let gix_id: gix::hash::ObjectId = (&id).try_into()?;
        let blob = repo
            .find_blob(gix_id)
            .map_err(|_| GitError::ObjectNotFound(id.as_str().to_string()))?;

        if blob.data.len() > LFS_POINTER_MAX_SIZE {
            return Ok(false);
        }

        let data = &blob.data;
        Ok(data.starts_with(LFS_POINTER_PREFIX)
            && contains_subslice(data, LFS_OID_MARKER))
    }

    pub fn tree_entries(&self, tree: ObjectId) -> GitResult<Vec<TreeEntry>> {
        let repo = self.gix_repo()?;
        let gix_id: gix::hash::ObjectId = (&tree).try_into()?;
        let gix_tree = repo.find_tree(gix_id)?;

        let mut entries = Vec::new();
        for entry_result in gix_tree.iter() {
            let entry = entry_result?;
            let name = entry.inner.filename.to_string();
            let oid = ObjectId::new(entry.inner.oid.to_hex().to_string());

            let is_tree = entry.inner.mode.is_tree();
            let is_lfs = if !is_tree {
                self.blob_is_lfs_pointer(oid.clone()).unwrap_or(false)
            } else {
                false
            };

            let kind = if is_tree {
                TreeKind::Tree
            } else if is_lfs {
                TreeKind::LfsPointer
            } else {
                TreeKind::Blob
            };

            let filemode = entry.inner.mode.value() as u32;

            let is_binary = match kind {
                TreeKind::LfsPointer => false,
                TreeKind::Blob => {
                    self.blob_is_binary(oid.clone()).unwrap_or(false)
                }
                TreeKind::Tree => false,
            };

            entries.push(TreeEntry {
                name,
                oid,
                kind,
                filemode,
                is_binary,
                is_lfs,
            });
        }

        Ok(entries)
    }

    pub fn tree_entry_by_path(
        &self,
        tree: ObjectId,
        path: String,
    ) -> GitResult<TreeEntry> {
        let repo = self.gix_repo()?;
        let gix_id: gix::hash::ObjectId = (&tree).try_into()?;
        let gix_tree = repo.find_tree(gix_id)?;

        let entry = gix_tree.find_entry(path.as_str()).ok_or_else(|| {
            GitError::ObjectNotFound(format!(
                "path '{}' not found in tree {}",
                path, tree
            ))
        })?;

        let name = entry.inner.filename.to_string();
        let oid = ObjectId::new(entry.inner.oid.to_hex().to_string());

        let is_tree = entry.inner.mode.is_tree();
        let is_lfs = if !is_tree {
            self.blob_is_lfs_pointer(oid.clone()).unwrap_or(false)
        } else {
            false
        };

        let kind = if is_tree {
            TreeKind::Tree
        } else if is_lfs {
            TreeKind::LfsPointer
        } else {
            TreeKind::Blob
        };

        let filemode = entry.inner.mode.value() as u32;

        let is_binary = match kind {
            TreeKind::LfsPointer => false,
            TreeKind::Blob => self.blob_is_binary(oid.clone()).unwrap_or(false),
            TreeKind::Tree => false,
        };

        Ok(TreeEntry {
            name,
            oid,
            kind,
            filemode,
            is_binary,
            is_lfs,
        })
    }
}

fn contains_subslice(data: &[u8], pattern: &[u8]) -> bool {
    if pattern.len() > data.len() {
        return false;
    }
    for window in data.windows(pattern.len()) {
        if window == pattern {
            return true;
        }
    }
    false
}
