//! Low-level operations on individual commits.
use std::{
    fmt::{Debug, Formatter},
    slice::Chunks,
};

use crate::{
    file::{self, EXTENDED_EDGES_MASK, LAST_EXTENDED_EDGE_MASK, NO_PARENT},
    File, Position,
};

/// The error used in the [`file::commit`][self] module.
#[derive(thiserror::Error, Debug)]
#[allow(missing_docs)]
pub enum Error {
    #[error("commit {0}'s extra edges overflows the commit-graph file's extra edges list")]
    ExtraEdgesListOverflow(gix_hash::ObjectId),
    #[error("commit {0}'s first parent is an extra edge index, which is invalid")]
    FirstParentIsExtraEdgeIndex(gix_hash::ObjectId),
    #[error("commit {0} has extra edges, but commit-graph file has no extra edges list")]
    MissingExtraEdgesList(gix_hash::ObjectId),
    #[error("commit {0} has a second parent but not a first parent")]
    SecondParentWithoutFirstParent(gix_hash::ObjectId),
}

/// A commit as stored in a [`File`].
#[derive(Copy, Clone)]
pub struct Commit<'a> {
    file: &'a File,
    pos: file::Position,
    // We can parse the below fields lazily if needed.
    commit_timestamp: u64,
    generation: u32,
    parent1: ParentEdge,
    parent2: ParentEdge,
    root_tree_id: &'a gix_hash::oid,
}

#[inline]
fn read_u32(b: &[u8]) -> u32 {
    u32::from_be_bytes(b.try_into().unwrap())
}

impl<'a> Commit<'a> {
    pub(crate) fn new(file: &'a File, pos: file::Position) -> Self {
        let bytes = file.commit_data_bytes(pos);
        Commit {
            file,
            pos,
            root_tree_id: gix_hash::oid::from_bytes_unchecked(&bytes[..file.hash_len]),
            parent1: ParentEdge::from_raw(read_u32(&bytes[file.hash_len..][..4])),
            parent2: ParentEdge::from_raw(read_u32(&bytes[file.hash_len + 4..][..4])),
            // TODO: Add support for corrected commit date offset overflow.
            //      See https://github.com/git/git/commit/e8b63005c48696a26f976f5f9b0ccaf1983e439d and
            //          https://github.com/git/git/commit/f90fca638e99a031dce8e3aca72427b2f9b4bb38 for more details and hints at a test.
            generation: read_u32(&bytes[file.hash_len + 8..][..4]) >> 2,
            commit_timestamp: u64::from_be_bytes(bytes[file.hash_len + 8..][..8].try_into().unwrap())
                & 0x0003_ffff_ffff,
        }
    }

    /// Returns the committer timestamp of this commit.
    ///
    /// The value is the number of seconds since 1970-01-01 00:00:00 UTC.
    pub fn committer_timestamp(&self) -> u64 {
        self.commit_timestamp
    }

    /// Returns the generation number of this commit.
    ///
    /// Commits without parents have generation number 1. Commits with parents have a generation
    /// number that is the max of their parents' generation numbers + 1.
    pub fn generation(&self) -> u32 {
        self.generation
    }

    /// Returns an iterator over the parent positions for lookup in the owning [Graph][crate::Graph].
    pub fn iter_parents(self) -> Parents<'a> {
        // I didn't find a combinator approach that a) was as strict as ParentIterator, b) supported
        // fuse-after-first-error behavior, and b) was significantly shorter or more understandable
        // than ParentIterator. So here we are.
        Parents {
            commit_data: self,
            state: ParentIteratorState::First,
        }
    }

    /// Returns the hash of this commit.
    pub fn id(&self) -> &'a gix_hash::oid {
        self.file.id_at(self.pos)
    }

    /// Returns the first parent of this commit.
    pub fn parent1(&self) -> Result<Option<Position>, Error> {
        self.iter_parents().next().transpose()
    }

    /// Returns the position at which this commit is stored in the parent [File].
    pub fn position(&self) -> file::Position {
        self.pos
    }

    /// Return the hash of the tree this commit points to.
    pub fn root_tree_id(&self) -> &gix_hash::oid {
        self.root_tree_id
    }
}

impl<'a> Debug for Commit<'a> {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        write!(
            f,
            "Commit {{ id: {}, lex_pos: {}, generation: {}, root_tree_id: {}, parent1: {:?}, parent2: {:?} }}",
            self.id(),
            self.pos,
            self.generation(),
            self.root_tree_id(),
            self.parent1,
            self.parent2,
        )
    }
}

impl<'a> Eq for Commit<'a> {}

impl<'a> PartialEq for Commit<'a> {
    fn eq(&self, other: &Self) -> bool {
        std::ptr::eq(self.file, other.file) && self.pos == other.pos
    }
}

/// An iterator over parents of a [`Commit`].
pub struct Parents<'a> {
    commit_data: Commit<'a>,
    state: ParentIteratorState<'a>,
}

impl<'a> Iterator for Parents<'a> {
    type Item = Result<Position, Error>;

    fn next(&mut self) -> Option<Self::Item> {
        let state = std::mem::replace(&mut self.state, ParentIteratorState::Exhausted);
        match state {
            ParentIteratorState::First => match self.commit_data.parent1 {
                ParentEdge::None => match self.commit_data.parent2 {
                    ParentEdge::None => None,
                    _ => Some(Err(Error::SecondParentWithoutFirstParent(self.commit_data.id().into()))),
                },
                ParentEdge::GraphPosition(pos) => {
                    self.state = ParentIteratorState::Second;
                    Some(Ok(pos))
                }
                ParentEdge::ExtraEdgeIndex(_) => {
                    Some(Err(Error::FirstParentIsExtraEdgeIndex(self.commit_data.id().into())))
                }
            },
            ParentIteratorState::Second => match self.commit_data.parent2 {
                ParentEdge::None => None,
                ParentEdge::GraphPosition(pos) => Some(Ok(pos)),
                ParentEdge::ExtraEdgeIndex(extra_edge_index) => {
                    if let Some(extra_edges_list) = self.commit_data.file.extra_edges_data() {
                        let start_offset: usize = extra_edge_index
                            .try_into()
                            .expect("an architecture able to hold 32 bits of integer");
                        let start_offset = start_offset
                            .checked_mul(4)
                            .expect("an extended edge index small enough to fit in usize");
                        if let Some(tail) = extra_edges_list.get(start_offset..) {
                            self.state = ParentIteratorState::Extra(tail.chunks(4));
                            // This recursive call is what blocks me from replacing ParentIterator
                            // with a std::iter::from_fn closure.
                            self.next()
                        } else {
                            Some(Err(Error::ExtraEdgesListOverflow(self.commit_data.id().into())))
                        }
                    } else {
                        Some(Err(Error::MissingExtraEdgesList(self.commit_data.id().into())))
                    }
                }
            },
            ParentIteratorState::Extra(mut chunks) => {
                if let Some(chunk) = chunks.next() {
                    let extra_edge = read_u32(chunk);
                    match ExtraEdge::from_raw(extra_edge) {
                        ExtraEdge::Internal(pos) => {
                            self.state = ParentIteratorState::Extra(chunks);
                            Some(Ok(pos))
                        }
                        ExtraEdge::Last(pos) => Some(Ok(pos)),
                    }
                } else {
                    Some(Err(Error::ExtraEdgesListOverflow(self.commit_data.id().into())))
                }
            }
            ParentIteratorState::Exhausted => None,
        }
    }

    fn size_hint(&self) -> (usize, Option<usize>) {
        match (&self.state, self.commit_data.parent1, self.commit_data.parent2) {
            (ParentIteratorState::First, ParentEdge::None, ParentEdge::None) => (0, Some(0)),
            (ParentIteratorState::First, ParentEdge::None, _) => (1, Some(1)),
            (ParentIteratorState::First, ParentEdge::GraphPosition(_), ParentEdge::None) => (1, Some(1)),
            (ParentIteratorState::First, ParentEdge::GraphPosition(_), ParentEdge::GraphPosition(_)) => (2, Some(2)),
            (ParentIteratorState::First, ParentEdge::GraphPosition(_), ParentEdge::ExtraEdgeIndex(_)) => (3, None),
            (ParentIteratorState::First, ParentEdge::ExtraEdgeIndex(_), _) => (1, Some(1)),
            (ParentIteratorState::Second, _, ParentEdge::None) => (0, Some(0)),
            (ParentIteratorState::Second, _, ParentEdge::GraphPosition(_)) => (1, Some(1)),
            (ParentIteratorState::Second, _, ParentEdge::ExtraEdgeIndex(_)) => (2, None),
            (ParentIteratorState::Extra(_), _, _) => (1, None),
            (ParentIteratorState::Exhausted, _, _) => (0, Some(0)),
        }
    }
}

#[derive(Debug)]
enum ParentIteratorState<'a> {
    First,
    Second,
    Extra(Chunks<'a, u8>),
    Exhausted,
}

#[derive(Clone, Copy, Debug)]
enum ParentEdge {
    None,
    GraphPosition(Position),
    ExtraEdgeIndex(u32),
}

impl ParentEdge {
    pub fn from_raw(raw: u32) -> ParentEdge {
        if raw == NO_PARENT {
            return ParentEdge::None;
        }
        if raw & EXTENDED_EDGES_MASK != 0 {
            ParentEdge::ExtraEdgeIndex(raw & !EXTENDED_EDGES_MASK)
        } else {
            ParentEdge::GraphPosition(Position(raw))
        }
    }
}

enum ExtraEdge {
    Internal(Position),
    Last(Position),
}

impl ExtraEdge {
    pub fn from_raw(raw: u32) -> Self {
        if raw & LAST_EXTENDED_EDGE_MASK != 0 {
            Self::Last(Position(raw & !LAST_EXTENDED_EDGE_MASK))
        } else {
            Self::Internal(Position(raw))
        }
    }
}
