use std::{borrow::BorrowMut, collections::VecDeque};

use gix_object::{tree::EntryRef, FindExt, TreeRefIter};

use crate::tree::{
    visit::{Change, ChangeId, Relation},
    Error, State, TreeInfoTuple, Visit,
};

/// Calculate the changes that would need to be applied to `lhs` to get `rhs` using `objects` to obtain objects as needed for traversal.
/// `state` can be used between multiple calls to re-use memory.
///
/// * The `state` maybe owned or mutably borrowed to allow reuses allocated data structures through multiple runs.
/// * `delegate` will receive the computed changes, see the [`Visit`] trait for more information on what to expect.
///
/// # Notes
///
/// * `lhs` can be an empty tree to simulate what would happen if the left-hand side didn't exist.
/// * To obtain progress, implement it within the `delegate`.
/// * Tree entries are expected to be ordered using [`tree-entry-comparison`][git_cmp_c] (the same [in Rust][git_cmp_rs])
/// * it does a breadth first iteration as buffer space only fits two trees, the current one on the one we compare with.
/// * does not do rename tracking but attempts to reduce allocations to zero (so performance is mostly determined
///   by the delegate implementation which should be as specific as possible. Rename tracking can be computed on top of the changes
///   received by the `delegate`.
/// * cycle checking is not performed, but can be performed in the delegate which can return
///   [`std::ops::ControlFlow::Break`] to stop the traversal.
///
/// [git_cmp_c]: https://github.com/git/git/blob/ef8ce8f3d4344fd3af049c17eeba5cd20d98b69f/tree-diff.c#L72-L88
/// [git_cmp_rs]: https://github.com/GitoxideLabs/gitoxide/blob/795962b107d86f58b1f7c75006da256d19cc80ad/gix-object/src/tree/mod.rs#L263-L273
#[doc(alias = "diff_tree_to_tree", alias = "git2")]
pub fn diff<StateMut>(
    lhs: TreeRefIter<'_>,
    rhs: TreeRefIter<'_>,
    mut state: StateMut,
    objects: impl gix_object::Find,
    delegate: &mut impl Visit,
) -> Result<(), Error>
where
    StateMut: BorrowMut<State>,
{
    let state = state.borrow_mut();
    state.clear();
    let mut lhs_entries = peekable(lhs);
    let mut rhs_entries = peekable(rhs);
    let mut relation = None;
    let mut pop_path = false;

    loop {
        if pop_path {
            delegate.pop_path_component();
        }
        pop_path = true;

        match (lhs_entries.next(), rhs_entries.next()) {
            (None, None) => {
                match state.trees.pop_front() {
                    Some((None, Some(rhs), relation_to_propagate)) => {
                        delegate.pop_front_tracked_path_and_set_current();
                        relation = relation_to_propagate;
                        rhs_entries = peekable(objects.find_tree_iter(&rhs, &mut state.buf2)?);
                    }
                    Some((Some(lhs), Some(rhs), relation_to_propagate)) => {
                        delegate.pop_front_tracked_path_and_set_current();
                        lhs_entries = peekable(objects.find_tree_iter(&lhs, &mut state.buf1)?);
                        rhs_entries = peekable(objects.find_tree_iter(&rhs, &mut state.buf2)?);
                        relation = relation_to_propagate;
                    }
                    Some((Some(lhs), None, relation_to_propagate)) => {
                        delegate.pop_front_tracked_path_and_set_current();
                        lhs_entries = peekable(objects.find_tree_iter(&lhs, &mut state.buf1)?);
                        relation = relation_to_propagate;
                    }
                    Some((None, None, _)) => unreachable!("BUG: it makes no sense to fill the stack with empties"),
                    None => return Ok(()),
                }
                pop_path = false;
            }
            (Some(lhs), Some(rhs)) => {
                use std::cmp::Ordering::*;
                let (lhs, rhs) = (lhs?, rhs?);
                match compare(&lhs, &rhs) {
                    Equal => handle_lhs_and_rhs_with_equal_filenames(
                        lhs,
                        rhs,
                        &mut state.trees,
                        &mut state.change_id,
                        relation,
                        delegate,
                    )?,
                    Less => catchup_lhs_with_rhs(
                        &mut lhs_entries,
                        lhs,
                        rhs,
                        &mut state.trees,
                        &mut state.change_id,
                        relation,
                        delegate,
                    )?,
                    Greater => catchup_rhs_with_lhs(
                        &mut rhs_entries,
                        lhs,
                        rhs,
                        &mut state.trees,
                        &mut state.change_id,
                        relation,
                        delegate,
                    )?,
                }
            }
            (Some(lhs), None) => {
                let lhs = lhs?;
                delete_entry_schedule_recursion(lhs, &mut state.trees, &mut state.change_id, relation, delegate)?;
            }
            (None, Some(rhs)) => {
                let rhs = rhs?;
                add_entry_schedule_recursion(rhs, &mut state.trees, &mut state.change_id, relation, delegate)?;
            }
        }
    }
}

fn compare(a: &EntryRef<'_>, b: &EntryRef<'_>) -> std::cmp::Ordering {
    let common = a.filename.len().min(b.filename.len());
    a.filename[..common].cmp(&b.filename[..common]).then_with(|| {
        let a = a.filename.get(common).or_else(|| a.mode.is_tree().then_some(&b'/'));
        let b = b.filename.get(common).or_else(|| b.mode.is_tree().then_some(&b'/'));
        a.cmp(&b)
    })
}

fn delete_entry_schedule_recursion(
    entry: EntryRef<'_>,
    queue: &mut VecDeque<TreeInfoTuple>,
    change_id: &mut ChangeId,
    relation_to_propagate: Option<Relation>,
    delegate: &mut impl Visit,
) -> Result<(), Error> {
    delegate.push_path_component(entry.filename);
    let relation = relation_to_propagate.or_else(|| {
        entry.mode.is_tree().then(|| {
            *change_id += 1;
            Relation::Parent(*change_id)
        })
    });
    let is_cancelled = delegate
        .visit(Change::Deletion {
            entry_mode: entry.mode,
            oid: entry.oid.to_owned(),
            relation,
        })
        .is_break();
    if is_cancelled {
        return Err(Error::Cancelled);
    }
    if entry.mode.is_tree() {
        delegate.pop_path_component();
        delegate.push_back_tracked_path_component(entry.filename);
        queue.push_back((Some(entry.oid.to_owned()), None, to_child(relation)));
    }
    Ok(())
}

fn add_entry_schedule_recursion(
    entry: EntryRef<'_>,
    queue: &mut VecDeque<TreeInfoTuple>,
    change_id: &mut ChangeId,
    relation_to_propagate: Option<Relation>,
    delegate: &mut impl Visit,
) -> Result<(), Error> {
    delegate.push_path_component(entry.filename);
    let relation = relation_to_propagate.or_else(|| {
        entry.mode.is_tree().then(|| {
            *change_id += 1;
            Relation::Parent(*change_id)
        })
    });
    if delegate
        .visit(Change::Addition {
            entry_mode: entry.mode,
            oid: entry.oid.to_owned(),
            relation,
        })
        .is_break()
    {
        return Err(Error::Cancelled);
    }
    if entry.mode.is_tree() {
        delegate.pop_path_component();
        delegate.push_back_tracked_path_component(entry.filename);
        queue.push_back((None, Some(entry.oid.to_owned()), to_child(relation)));
    }
    Ok(())
}

fn catchup_rhs_with_lhs(
    rhs_entries: &mut IteratorType<TreeRefIter<'_>>,
    lhs: EntryRef<'_>,
    rhs: EntryRef<'_>,
    queue: &mut VecDeque<TreeInfoTuple>,
    change_id: &mut ChangeId,
    relation_to_propagate: Option<Relation>,
    delegate: &mut impl Visit,
) -> Result<(), Error> {
    use std::cmp::Ordering::*;
    add_entry_schedule_recursion(rhs, queue, change_id, relation_to_propagate, delegate)?;
    loop {
        match rhs_entries.peek() {
            Some(Ok(rhs)) => match compare(&lhs, rhs) {
                Equal => {
                    let rhs = rhs_entries.next().transpose()?.expect("the peeked item to be present");
                    delegate.pop_path_component();
                    handle_lhs_and_rhs_with_equal_filenames(
                        lhs,
                        rhs,
                        queue,
                        change_id,
                        relation_to_propagate,
                        delegate,
                    )?;
                    break;
                }
                Greater => {
                    let rhs = rhs_entries.next().transpose()?.expect("the peeked item to be present");
                    delegate.pop_path_component();
                    add_entry_schedule_recursion(rhs, queue, change_id, relation_to_propagate, delegate)?;
                }
                Less => {
                    delegate.pop_path_component();
                    delete_entry_schedule_recursion(lhs, queue, change_id, relation_to_propagate, delegate)?;
                    break;
                }
            },
            Some(Err(err)) => return Err(Error::EntriesDecode(err.to_owned())),
            None => {
                delegate.pop_path_component();
                delete_entry_schedule_recursion(lhs, queue, change_id, relation_to_propagate, delegate)?;
                break;
            }
        }
    }
    Ok(())
}

fn catchup_lhs_with_rhs(
    lhs_entries: &mut IteratorType<TreeRefIter<'_>>,
    lhs: EntryRef<'_>,
    rhs: EntryRef<'_>,
    queue: &mut VecDeque<TreeInfoTuple>,
    change_id: &mut ChangeId,
    relation_to_propagate: Option<Relation>,
    delegate: &mut impl Visit,
) -> Result<(), Error> {
    use std::cmp::Ordering::*;
    delete_entry_schedule_recursion(lhs, queue, change_id, relation_to_propagate, delegate)?;
    loop {
        match lhs_entries.peek() {
            Some(Ok(lhs)) => match compare(lhs, &rhs) {
                Equal => {
                    let lhs = lhs_entries.next().expect("the peeked item to be present")?;
                    delegate.pop_path_component();
                    handle_lhs_and_rhs_with_equal_filenames(
                        lhs,
                        rhs,
                        queue,
                        change_id,
                        relation_to_propagate,
                        delegate,
                    )?;
                    break;
                }
                Less => {
                    let lhs = lhs_entries.next().expect("the peeked item to be present")?;
                    delegate.pop_path_component();
                    delete_entry_schedule_recursion(lhs, queue, change_id, relation_to_propagate, delegate)?;
                }
                Greater => {
                    delegate.pop_path_component();
                    add_entry_schedule_recursion(rhs, queue, change_id, relation_to_propagate, delegate)?;
                    break;
                }
            },
            Some(Err(err)) => return Err(Error::EntriesDecode(err.to_owned())),
            None => {
                delegate.pop_path_component();
                add_entry_schedule_recursion(rhs, queue, change_id, relation_to_propagate, delegate)?;
                break;
            }
        }
    }
    Ok(())
}

fn handle_lhs_and_rhs_with_equal_filenames(
    lhs: EntryRef<'_>,
    rhs: EntryRef<'_>,
    queue: &mut VecDeque<TreeInfoTuple>,
    change_id: &mut ChangeId,
    relation_to_propagate: Option<Relation>,
    delegate: &mut impl Visit,
) -> Result<(), Error> {
    match (lhs.mode.is_tree(), rhs.mode.is_tree()) {
        (true, true) => {
            if lhs.oid == rhs.oid {
                // If the tree oids are identical, we won't bother recursing
                // into this subtree as the entire tree is identical.
                // For path management purposes, treat it like a skipped blob.
                delegate.push_path_component(lhs.filename);
            } else {
                delegate.push_back_tracked_path_component(lhs.filename);
                if delegate
                    .visit(Change::Modification {
                        previous_entry_mode: lhs.mode,
                        previous_oid: lhs.oid.to_owned(),
                        entry_mode: rhs.mode,
                        oid: rhs.oid.to_owned(),
                    })
                    .is_break()
                {
                    return Err(Error::Cancelled);
                }
                queue.push_back((
                    Some(lhs.oid.to_owned()),
                    Some(rhs.oid.to_owned()),
                    relation_to_propagate,
                ));
            }
        }
        (_, true) => {
            delegate.push_back_tracked_path_component(lhs.filename);
            if delegate
                .visit(Change::Deletion {
                    entry_mode: lhs.mode,
                    oid: lhs.oid.to_owned(),
                    relation: None,
                })
                .is_break()
            {
                return Err(Error::Cancelled);
            }

            let relation = relation_to_propagate.or_else(|| {
                *change_id += 1;
                Some(Relation::Parent(*change_id))
            });
            if delegate
                .visit(Change::Addition {
                    entry_mode: rhs.mode,
                    oid: rhs.oid.to_owned(),
                    relation,
                })
                .is_break()
            {
                return Err(Error::Cancelled);
            }
            queue.push_back((None, Some(rhs.oid.to_owned()), to_child(relation)));
        }
        (true, _) => {
            delegate.push_back_tracked_path_component(lhs.filename);
            let relation = relation_to_propagate.or_else(|| {
                *change_id += 1;
                Some(Relation::Parent(*change_id))
            });
            if delegate
                .visit(Change::Deletion {
                    entry_mode: lhs.mode,
                    oid: lhs.oid.to_owned(),
                    relation,
                })
                .is_break()
            {
                return Err(Error::Cancelled);
            }
            if delegate
                .visit(Change::Addition {
                    entry_mode: rhs.mode,
                    oid: rhs.oid.to_owned(),
                    relation: None,
                })
                .is_break()
            {
                return Err(Error::Cancelled);
            }
            queue.push_back((Some(lhs.oid.to_owned()), None, to_child(relation)));
        }
        (false, false) => {
            delegate.push_path_component(lhs.filename);
            debug_assert!(lhs.mode.is_no_tree() && lhs.mode.is_no_tree());
            if (lhs.oid != rhs.oid || lhs.mode != rhs.mode)
                && delegate
                    .visit(Change::Modification {
                        previous_entry_mode: lhs.mode,
                        previous_oid: lhs.oid.to_owned(),
                        entry_mode: rhs.mode,
                        oid: rhs.oid.to_owned(),
                    })
                    .is_break()
            {
                return Err(Error::Cancelled);
            }
        }
    }
    Ok(())
}

type IteratorType<I> = std::iter::Peekable<I>;

fn to_child(r: Option<Relation>) -> Option<Relation> {
    r.map(|r| match r {
        Relation::Parent(id) => Relation::ChildOfParent(id),
        Relation::ChildOfParent(id) => Relation::ChildOfParent(id),
    })
}

fn peekable<I: Iterator>(iter: I) -> IteratorType<I> {
    iter.peekable()
}

#[cfg(test)]
mod tests {
    use std::cmp::Ordering;

    use gix_object::tree::EntryKind;

    use super::*;

    #[test]
    fn compare_select_samples() {
        let null = gix_hash::ObjectId::null(gix_hash::Kind::Sha1);
        let actual = compare(
            &EntryRef {
                mode: EntryKind::Blob.into(),
                filename: "plumbing-cli.rs".into(),
                oid: &null,
            },
            &EntryRef {
                mode: EntryKind::Tree.into(),
                filename: "plumbing".into(),
                oid: &null,
            },
        );
        assert_eq!(actual, Ordering::Less);
        let actual = compare(
            &EntryRef {
                mode: EntryKind::Tree.into(),
                filename: "plumbing-cli.rs".into(),
                oid: &null,
            },
            &EntryRef {
                mode: EntryKind::Blob.into(),
                filename: "plumbing".into(),
                oid: &null,
            },
        );
        assert_eq!(actual, Ordering::Greater);
    }
}
