#![allow(clippy::result_large_err)]
use std::{convert::TryInto, ops::DerefMut};

use gix_hash::ObjectId;
use gix_macros::momo;
use gix_odb::{Find, FindExt, Header, HeaderExt, Write};
use gix_ref::{
    transaction::{LogChange, PreviousValue, RefLog},
    FullName,
};
use smallvec::SmallVec;

use crate::{commit, ext::ObjectIdExt, object, tag, Blob, Id, Object, Reference, Tree};

/// Methods related to object creation.
impl crate::Repository {
    /// Find the object with `id` in the object database or return an error if it could not be found.
    ///
    /// There are various legitimate reasons for an object to not be present, which is why
    /// [`try_find_object(…)`][crate::Repository::try_find_object()] might be preferable instead.
    ///
    /// # Performance Note
    ///
    /// In order to get the kind of the object, is must be fully decoded from storage if it is packed with deltas.
    /// Loose object could be partially decoded, even though that's not implemented.
    #[momo]
    pub fn find_object(&self, id: impl Into<ObjectId>) -> Result<Object<'_>, object::find::existing::Error> {
        let id = id.into();
        if id == ObjectId::empty_tree(self.object_hash()) {
            return Ok(Object {
                id,
                kind: gix_object::Kind::Tree,
                data: Vec::new(),
                repo: self,
            });
        }
        let mut buf = self.free_buf();
        let kind = self.objects.find(&id, &mut buf)?.kind;
        Ok(Object::from_data(id, kind, buf, self))
    }

    /// Obtain information about an object without fully decoding it, or fail if the object doesn't exist.
    ///
    /// Note that despite being cheaper than [`Self::find_object()`], there is still some effort traversing delta-chains.
    #[doc(alias = "read_header", alias = "git2")]
    #[momo]
    pub fn find_header(&self, id: impl Into<ObjectId>) -> Result<gix_odb::find::Header, object::find::existing::Error> {
        let id = id.into();
        if id == ObjectId::empty_tree(self.object_hash()) {
            return Ok(gix_odb::find::Header::Loose {
                kind: gix_object::Kind::Tree,
                size: 0,
            });
        }
        self.objects.header(id)
    }

    /// Return `true` if `id` exists in the object database.
    ///
    /// # Performance
    ///
    /// This method can be slow if the underlying [object database](crate::Repository::objects) has
    /// an unsuitable [RefreshMode](gix_odb::store::RefreshMode) and `id` is not likely to exist.
    /// Use [`repo.objects.refresh_never()`](gix_odb::store::Handle::refresh_never) to avoid expensive
    /// IO-bound refreshes if an object wasn't found.
    #[doc(alias = "exists", alias = "git2")]
    #[momo]
    pub fn has_object(&self, id: impl AsRef<gix_hash::oid>) -> bool {
        let id = id.as_ref();
        if id == ObjectId::empty_tree(self.object_hash()) {
            true
        } else {
            self.objects.contains(id)
        }
    }

    /// Obtain information about an object without fully decoding it, or `None` if the object doesn't exist.
    ///
    /// Note that despite being cheaper than [`Self::try_find_object()`], there is still some effort traversing delta-chains.
    #[momo]
    pub fn try_find_header(
        &self,
        id: impl Into<ObjectId>,
    ) -> Result<Option<gix_odb::find::Header>, object::find::Error> {
        let id = id.into();
        if id == ObjectId::empty_tree(self.object_hash()) {
            return Ok(Some(gix_odb::find::Header::Loose {
                kind: gix_object::Kind::Tree,
                size: 0,
            }));
        }
        self.objects.try_header(&id).map_err(Into::into)
    }

    /// Try to find the object with `id` or return `None` if it wasn't found.
    #[momo]
    pub fn try_find_object(&self, id: impl Into<ObjectId>) -> Result<Option<Object<'_>>, object::find::Error> {
        let id = id.into();
        if id == ObjectId::empty_tree(self.object_hash()) {
            return Ok(Some(Object {
                id,
                kind: gix_object::Kind::Tree,
                data: Vec::new(),
                repo: self,
            }));
        }

        let mut buf = self.free_buf();
        match self.objects.try_find(&id, &mut buf)? {
            Some(obj) => {
                let kind = obj.kind;
                Ok(Some(Object::from_data(id, kind, buf, self)))
            }
            None => Ok(None),
        }
    }

    fn shared_empty_buf(&self) -> std::cell::RefMut<'_, Vec<u8>> {
        let mut bufs = self.bufs.borrow_mut();
        if bufs.last().is_none() {
            bufs.push(Vec::with_capacity(512));
        }
        std::cell::RefMut::map(bufs, |bufs| {
            let buf = bufs.last_mut().expect("we assure one is present");
            buf.clear();
            buf
        })
    }

    /// Write the given object into the object database and return its object id.
    ///
    /// Note that we hash the object in memory to avoid storing objects that are already present. That way,
    /// we avoid writing duplicate objects using slow disks that will eventually have to be garbage collected.
    pub fn write_object(&self, object: impl gix_object::WriteTo) -> Result<Id<'_>, object::write::Error> {
        let mut buf = self.shared_empty_buf();
        object.write_to(buf.deref_mut()).expect("write to memory works");

        self.write_object_inner(&buf, object.kind())
    }

    fn write_object_inner(&self, buf: &[u8], kind: gix_object::Kind) -> Result<Id<'_>, object::write::Error> {
        let oid = gix_object::compute_hash(self.object_hash(), kind, buf);
        if self.objects.contains(&oid) {
            return Ok(oid.attach(self));
        }

        self.objects
            .write_buf(kind, buf)
            .map(|oid| oid.attach(self))
            .map_err(Into::into)
    }

    /// Write a blob from the given `bytes`.
    ///
    /// We avoid writing duplicate objects to slow disks that will eventually have to be garbage collected by
    /// pre-hashing the data, and checking if the object is already present.
    #[momo]
    pub fn write_blob(&self, bytes: impl AsRef<[u8]>) -> Result<Id<'_>, object::write::Error> {
        let bytes = bytes.as_ref();
        let oid = gix_object::compute_hash(self.object_hash(), gix_object::Kind::Blob, bytes);
        if self.objects.contains(&oid) {
            return Ok(oid.attach(self));
        }
        self.objects
            .write_buf(gix_object::Kind::Blob, bytes)
            .map_err(Into::into)
            .map(|oid| oid.attach(self))
    }

    /// Write a blob from the given `Read` implementation.
    ///
    /// Note that we hash the object in memory to avoid storing objects that are already present. That way,
    /// we avoid writing duplicate objects using slow disks that will eventually have to be garbage collected.
    ///
    /// If that is prohibitive, use the object database directly.
    pub fn write_blob_stream(
        &self,
        mut bytes: impl std::io::Read + std::io::Seek,
    ) -> Result<Id<'_>, object::write::Error> {
        let mut buf = self.shared_empty_buf();
        std::io::copy(&mut bytes, buf.deref_mut()).expect("write to memory works");

        self.write_blob_stream_inner(&buf)
    }

    fn write_blob_stream_inner(&self, buf: &[u8]) -> Result<Id<'_>, object::write::Error> {
        let oid = gix_object::compute_hash(self.object_hash(), gix_object::Kind::Blob, buf);
        if self.objects.contains(&oid) {
            return Ok(oid.attach(self));
        }

        self.objects
            .write_buf(gix_object::Kind::Blob, buf)
            .map_err(Into::into)
            .map(|oid| oid.attach(self))
    }

    /// Create a tag reference named `name` (without `refs/tags/` prefix) pointing to a newly created tag object
    /// which in turn points to `target` and return the newly created reference.
    ///
    /// It will be created with `constraint` which is most commonly to [only create it][PreviousValue::MustNotExist]
    /// or to [force overwriting a possibly existing tag](PreviousValue::Any).
    #[momo]
    pub fn tag(
        &self,
        name: impl AsRef<str>,
        target: impl AsRef<gix_hash::oid>,
        target_kind: gix_object::Kind,
        tagger: Option<gix_actor::SignatureRef<'_>>,
        message: impl AsRef<str>,
        constraint: PreviousValue,
    ) -> Result<Reference<'_>, tag::Error> {
        let tag = gix_object::Tag {
            target: target.as_ref().into(),
            target_kind,
            name: name.as_ref().into(),
            tagger: tagger.map(|t| t.to_owned()),
            message: message.as_ref().into(),
            pgp_signature: None,
        };
        let tag_id = self.write_object(&tag)?;
        self.tag_reference(name, tag_id, constraint).map_err(Into::into)
    }

    /// Similar to [`commit(…)`][crate::Repository::commit()], but allows to create the commit with `committer` and `author` specified.
    ///
    /// This forces setting the commit time and author time by hand. Note that typically, committer and author are the same.
    pub fn commit_as<'a, 'c, Name, E>(
        &self,
        committer: impl Into<gix_actor::SignatureRef<'c>>,
        author: impl Into<gix_actor::SignatureRef<'a>>,
        reference: Name,
        message: impl AsRef<str>,
        tree: impl Into<ObjectId>,
        parents: impl IntoIterator<Item = impl Into<ObjectId>>,
    ) -> Result<Id<'_>, commit::Error>
    where
        Name: TryInto<FullName, Error = E>,
        commit::Error: From<E>,
    {
        self.commit_as_inner(
            committer.into(),
            author.into(),
            reference.try_into()?,
            message.as_ref(),
            tree.into(),
            parents.into_iter().map(Into::into).collect(),
        )
    }

    fn commit_as_inner(
        &self,
        committer: gix_actor::SignatureRef<'_>,
        author: gix_actor::SignatureRef<'_>,
        reference: FullName,
        message: &str,
        tree: ObjectId,
        parents: SmallVec<[ObjectId; 1]>,
    ) -> Result<Id<'_>, commit::Error> {
        use gix_ref::{
            transaction::{Change, RefEdit},
            Target,
        };

        // TODO: possibly use CommitRef to save a few allocations (but will have to allocate for object ids anyway.
        //       This can be made vastly more efficient though if we wanted to, so we lie in the API
        let commit = gix_object::Commit {
            message: message.into(),
            tree,
            author: author.into(),
            committer: committer.into(),
            encoding: None,
            parents,
            extra_headers: Default::default(),
        };

        let commit_id = self.write_object(&commit)?;
        self.edit_reference(RefEdit {
            change: Change::Update {
                log: LogChange {
                    mode: RefLog::AndReference,
                    force_create_reflog: false,
                    message: crate::reference::log::message("commit", commit.message.as_ref(), commit.parents.len()),
                },
                expected: match commit.parents.first().map(|p| Target::Peeled(*p)) {
                    Some(previous) => {
                        if reference.as_bstr() == "HEAD" {
                            PreviousValue::MustExistAndMatch(previous)
                        } else {
                            PreviousValue::ExistingMustMatch(previous)
                        }
                    }
                    None => PreviousValue::MustNotExist,
                },
                new: Target::Peeled(commit_id.inner),
            },
            name: reference,
            deref: true,
        })?;
        Ok(commit_id)
    }

    /// Create a new commit object with `message` referring to `tree` with `parents`, and point `reference`
    /// to it. The commit is written without message encoding field, which can be assumed to be UTF-8.
    /// `author` and `committer` fields are pre-set from the configuration, which can be altered
    /// [temporarily][crate::Repository::config_snapshot_mut()] before the call if required.
    ///
    /// `reference` will be created if it doesn't exist, and can be `"HEAD"` to automatically write-through to the symbolic reference
    /// that `HEAD` points to if it is not detached. For this reason, detached head states cannot be created unless the `HEAD` is detached
    /// already. The reflog will be written as canonical git would do, like `<operation> (<detail>): <summary>`.
    ///
    /// The first parent id in `parents` is expected to be the current target of `reference` and the operation will fail if it is not.
    /// If there is no parent, the `reference` is expected to not exist yet.
    ///
    /// The method fails immediately if a `reference` lock can't be acquired.
    pub fn commit<Name, E>(
        &self,
        reference: Name,
        message: impl AsRef<str>,
        tree: impl Into<ObjectId>,
        parents: impl IntoIterator<Item = impl Into<ObjectId>>,
    ) -> Result<Id<'_>, commit::Error>
    where
        Name: TryInto<FullName, Error = E>,
        commit::Error: From<E>,
    {
        let author = self.author().ok_or(commit::Error::AuthorMissing)??;
        let committer = self.committer().ok_or(commit::Error::CommitterMissing)??;
        self.commit_as(committer, author, reference, message, tree, parents)
    }

    /// Return an empty tree object, suitable for [getting changes](Tree::changes()).
    ///
    /// Note that the returned object is special and doesn't necessarily physically exist in the object database.
    /// This means that this object can be used in an uninitialized, empty repository which would report to have no objects at all.
    pub fn empty_tree(&self) -> Tree<'_> {
        self.find_object(ObjectId::empty_tree(self.object_hash()))
            .expect("always present")
            .into_tree()
    }

    /// Return an empty blob object.
    ///
    /// Note that the returned object is special and doesn't necessarily physically exist in the object database.
    /// This means that this object can be used in an uninitialized, empty repository which would report to have no objects at all.
    pub fn empty_blob(&self) -> Blob<'_> {
        Blob {
            id: gix_hash::ObjectId::empty_blob(self.object_hash()),
            data: Vec::new(),
            repo: self,
        }
    }
}
