use gix::bstr::ByteSlice;

use crate::{
    bare::GitBare,
    cmd::{
        diff::{
            DiffOptions, DiffResult, DiffStats,
            diff_tree_to_tree::{
                HunkCollector, change_to_delta, matches_pathspec,
            },
        },
        oid::ObjectId,
    },
    errors::GitResult,
};

impl GitBare {
    pub fn diff_index_to_tree(
        &self,
        tree: ObjectId,
        opts: Option<DiffOptions>,
    ) -> GitResult<DiffResult> {
        let options = opts.unwrap_or_default();
        let repo = self.gix_repo()?;
        let gix_id: gix::hash::ObjectId = (&tree).try_into()?;
        let tree_obj = if let Ok(tree) = repo.find_tree(gix_id) {
            tree
        } else {
            let commit = repo
                .find_commit(gix_id)
                .map_err(|e| crate::errors::GitError::Gix(e.to_string()))?;
            let tree_id = commit
                .tree_id()
                .map_err(|e| crate::errors::GitError::Gix(e.to_string()))?
                .detach();
            repo.find_tree(tree_id)
                .map_err(|e| crate::errors::GitError::Gix(e.to_string()))?
        };
        let head_id = repo
            .head_id()
            .map_err(|e| crate::errors::GitError::Gix(e.to_string()))?;
        let head_commit = repo
            .find_commit(head_id.detach())
            .map_err(|e| crate::errors::GitError::Gix(e.to_string()))?;
        let head_tree_id = head_commit
            .tree_id()
            .map_err(|e| crate::errors::GitError::Gix(e.to_string()))?
            .detach();
        let head_tree = repo
            .find_tree(head_tree_id)
            .map_err(|e| crate::errors::GitError::Gix(e.to_string()))?;
        let tree_oid = tree_obj.id().detach();
        let head_tree_oid = head_tree.id().detach();
        if tree_oid == head_tree_oid {
            return Ok(DiffResult {
                stats: DiffStats {
                    files_changed: 0,
                    insertions: 0,
                    deletions: 0,
                },
                deltas: Vec::new(),
            });
        }

        let mut diff_opts = gix::diff::Options::default();
        diff_opts.track_path();

        let changes = repo
            .diff_tree_to_tree(&tree_obj, &head_tree, Some(diff_opts))
            .map_err(|e| crate::errors::GitError::Gix(e.to_string()))?;

        let mut resource_cache = repo
            .diff_resource_cache_for_tree_diff()
            .map_err(|e| crate::errors::GitError::Gix(e.to_string()))?;

        let mut deltas = Vec::new();
        let mut stats = DiffStats {
            files_changed: 0,
            insertions: 0,
            deletions: 0,
        };

        for change in &changes {
            let location = change.location().to_str().unwrap_or("");
            if !matches_pathspec(&options.pathspec, location) {
                continue;
            }

            let mut delta = change_to_delta(change);

            resource_cache
                .set_resource_by_change(change.to_ref(), &repo.objects)
                .map_err(|e| crate::errors::GitError::Gix(e.to_string()))?;

            let is_binary = {
                use gix::diff::blob::platform::prepare_diff::Operation;

                let prep = resource_cache
                    .prepare_diff()
                    .map_err(|e| crate::errors::GitError::Gix(e.to_string()))?;

                match prep.operation {
                    Operation::InternalDiff { algorithm } => {
                        let input = prep.interned_input();
                        let diff = gix::diff::blob::diff_with_slider_heuristics(
                            algorithm, &input,
                        );

                        stats.files_changed += 1;
                        stats.insertions += diff.count_additions() as usize;
                        stats.deletions += diff.count_removals() as usize;

                        if options.context_lines > 0 {
                            let ctx = gix::diff::blob::unified_diff::ContextSize::symmetrical(
                                options.context_lines.max(3),
                            );
                            let collector = HunkCollector::new();
                            let unified = gix::diff::blob::UnifiedDiff::new(
                                &diff, &input, collector, ctx,
                            );
                            let (hunks, lines) =
                                unified.consume().map_err(|e| {
                                    crate::errors::GitError::Gix(e.to_string())
                                })?;
                            delta.hunks = hunks;
                            delta.lines = lines;
                        }
                        false
                    }
                    Operation::SourceOrDestinationIsBinary => {
                        stats.files_changed += 1;
                        true
                    }
                    Operation::ExternalCommand { .. } => {
                        stats.files_changed += 1;
                        false
                    }
                }
            };

            if is_binary {
                delta.old_file.is_binary = true;
                delta.new_file.is_binary = true;
            }

            resource_cache.clear_resource_cache_keep_allocation();
            deltas.push(delta);
        }

        Ok(DiffResult { stats, deltas })
    }
}
