use crate::common::git;
use std::rc::Rc;
use super::{commit::Commit, frontend::Frontend, workspace::Workspace};

struct CommitIdSource<'a>(&'a [Rc<git::Description>]);

struct CommitIdIter<'a>(std::slice::Iter<'a, Rc<git::Description>>);

impl<'a> Iterator for CommitIdIter<'a> {
    type Item = &'a [u8];

    fn next(&mut self) -> Option<Self::Item> {
        self.0.next().map(|d| d.id.as_slice())
    }
}

impl<'a> imara_diff::TokenSource for CommitIdSource<'a> {
    type Token = &'a [u8];
    type Tokenizer = CommitIdIter<'a>;

    fn tokenize(&self) -> Self::Tokenizer {
        CommitIdIter(self.0.iter())
    }

    fn estimate_tokens(&self) -> u32 {
        self.0.len() as u32
    }
}

impl<FE: Frontend + 'static> Workspace<FE> {
    /// Adjust the commit list to match the given list `new_commits`. Commits are moved,
    /// deleted and added as necessary.
    pub fn change_commits(&self, new_commits: Vec<Rc<git::Description>>) {
        let (old_descs, _) = self.commit_descriptions().unwrap();
        let input = imara_diff::InternedInput::new(
            CommitIdSource(&old_descs),
            CommitIdSource(&new_commits),
        );
        let mut diff = imara_diff::Diff::compute(imara_diff::Algorithm::Histogram, &input);
        diff.postprocess_no_heuristic(&input);

        // First, remove commits but store them for later reinsertion. Record all added
        // commits (and the index they should go to); we'll add them in the next step.
        let mut jump_to: Option<usize> = None;
        let mut removed: Vec<Commit> = Vec::new();
        let mut to_add: Vec<(usize, Rc<git::Description>)> = Vec::new();
        let mut removed_count: usize = 0;
        let mut first = true;
        for hunk in diff.hunks() {
            if first {
                // Move the current commit before the first change.
                if self.jump_before_commit(hunk.before.start as usize).unwrap() {
                    jump_to = Some(hunk.before.start as usize);
                }
                first = false;
            }
            let remove_pos = hunk.before.start as usize - removed_count;
            for _ in hunk.before.clone() {
                removed.push(self.remove_commit(remove_pos));
            }
            let target = hunk.before.start as usize - removed_count;
            for i in hunk.after.clone() {
                to_add.push((target, new_commits[i as usize].clone()));
            }
            removed_count += (hunk.before.end - hunk.before.start) as usize;
        }

        // Second, add the commits, reinserting those that were removed.
        let mut added_count = 0;
        for (target, description) in to_add {
            let index = target + added_count;
            let matched = removed.iter().position(|c| c.description().id == description.id);
            if let Some(pos) = matched {
                let commit = removed.remove(pos);
                self.reinsert_commit(index, commit);
            } else {
                self.insert_commit(index, description).unwrap();
            }
            added_count += 1;
        }

        // If we moved the current commit, advance to the next one. Use the absolute index
        // rather than apply_next() to prevent race conditions with other parts of the GUI.
        if let Some(jump_to) = jump_to {
            self.jump_to_commit(jump_to).unwrap();
        }
    }

    /// Add commits to the workspace. Tries to add them to the right places.
    pub fn add_commits(&self, commits: Vec<Rc<git::Description>>) {
        let Some(start) = commits.iter().map(|desc| self.target_commit_position(desc)).min()
            else { return; };
        let jump_to = if self.jump_before_commit(start).unwrap() {
            Some(start)
        } else {
            None
        };
        for desc in commits {
            let pos = self.target_commit_position(&desc);
            self.insert_commit(pos, desc).unwrap();
        }
        if let Some(jump_to) = jump_to {
            self.jump_to_commit(jump_to).unwrap();
        }
    }
}
