use std::io;
use std::marker;
use std::mem::MaybeUninit;
use std::ptr;
use std::slice;

use std::ffi::CString;

use libc::{c_char, c_int, c_void, size_t};

use crate::panic;
use crate::util::Binding;
use crate::{raw, Error, IndexerProgress, Mempack, Object, ObjectType, Oid, Progress};

/// A structure to represent a git object database
pub struct Odb<'repo> {
    raw: *mut raw::git_odb,
    _marker: marker::PhantomData<Object<'repo>>,
}

impl<'repo> Binding for Odb<'repo> {
    type Raw = *mut raw::git_odb;

    unsafe fn from_raw(raw: *mut raw::git_odb) -> Odb<'repo> {
        Odb {
            raw: raw,
            _marker: marker::PhantomData,
        }
    }
    fn raw(&self) -> *mut raw::git_odb {
        self.raw
    }
}

impl<'repo> Drop for Odb<'repo> {
    fn drop(&mut self) {
        unsafe { raw::git_odb_free(self.raw) }
    }
}

impl<'repo> Odb<'repo> {
    /// Creates an object database without any backends.
    pub fn new<'a>() -> Result<Odb<'a>, Error> {
        unsafe {
            let mut out = ptr::null_mut();
            try_call!(raw::git_odb_new(&mut out));
            Ok(Odb::from_raw(out))
        }
    }

    /// Create object database reading stream.
    ///
    /// Note that most backends do not support streaming reads because they store their objects as compressed/delta'ed blobs.
    /// If the backend does not support streaming reads, use the `read` method instead.
    pub fn reader(&self, oid: Oid) -> Result<(OdbReader<'_>, usize, ObjectType), Error> {
        let mut out = ptr::null_mut();
        let mut size = 0usize;
        let mut otype: raw::git_object_t = ObjectType::Any.raw();
        unsafe {
            try_call!(raw::git_odb_open_rstream(
                &mut out,
                &mut size,
                &mut otype,
                self.raw,
                oid.raw()
            ));
            Ok((
                OdbReader::from_raw(out),
                size,
                ObjectType::from_raw(otype).unwrap(),
            ))
        }
    }

    /// Create object database writing stream.
    ///
    /// The type and final length of the object must be specified when opening the stream.
    /// If the backend does not support streaming writes, use the `write` method instead.
    pub fn writer(&self, size: usize, obj_type: ObjectType) -> Result<OdbWriter<'_>, Error> {
        let mut out = ptr::null_mut();
        unsafe {
            try_call!(raw::git_odb_open_wstream(
                &mut out,
                self.raw,
                size as raw::git_object_size_t,
                obj_type.raw()
            ));
            Ok(OdbWriter::from_raw(out))
        }
    }

    /// Iterate over all objects in the object database.s
    pub fn foreach<C>(&self, mut callback: C) -> Result<(), Error>
    where
        C: FnMut(&Oid) -> bool,
    {
        unsafe {
            let mut data = ForeachCbData {
                callback: &mut callback,
            };
            let cb: raw::git_odb_foreach_cb = Some(foreach_cb);
            try_call!(raw::git_odb_foreach(
                self.raw(),
                cb,
                &mut data as *mut _ as *mut _
            ));
            Ok(())
        }
    }

    /// Read an object from the database.
    pub fn read(&self, oid: Oid) -> Result<OdbObject<'_>, Error> {
        let mut out = ptr::null_mut();
        unsafe {
            try_call!(raw::git_odb_read(&mut out, self.raw, oid.raw()));
            Ok(OdbObject::from_raw(out))
        }
    }

    /// Reads the header of an object from the database
    /// without reading the full content.
    pub fn read_header(&self, oid: Oid) -> Result<(usize, ObjectType), Error> {
        let mut size: usize = 0;
        let mut kind_id: i32 = ObjectType::Any.raw();

        unsafe {
            try_call!(raw::git_odb_read_header(
                &mut size as *mut size_t,
                &mut kind_id as *mut raw::git_object_t,
                self.raw,
                oid.raw()
            ));

            Ok((size, ObjectType::from_raw(kind_id).unwrap()))
        }
    }

    /// Write an object to the database.
    pub fn write(&self, kind: ObjectType, data: &[u8]) -> Result<Oid, Error> {
        unsafe {
            let mut out = raw::git_oid {
                id: [0; raw::GIT_OID_RAWSZ],
            };
            try_call!(raw::git_odb_write(
                &mut out,
                self.raw,
                data.as_ptr() as *const c_void,
                data.len(),
                kind.raw()
            ));
            Ok(Oid::from_raw(&mut out))
        }
    }

    /// Create stream for writing a pack file to the ODB
    pub fn packwriter(&self) -> Result<OdbPackwriter<'_>, Error> {
        let mut out = ptr::null_mut();
        let progress = MaybeUninit::uninit();
        let progress_cb: raw::git_indexer_progress_cb = Some(write_pack_progress_cb);
        let progress_payload = Box::new(OdbPackwriterCb { cb: None });
        let progress_payload_ptr = Box::into_raw(progress_payload);

        unsafe {
            try_call!(raw::git_odb_write_pack(
                &mut out,
                self.raw,
                progress_cb,
                progress_payload_ptr as *mut c_void
            ));
        }

        Ok(OdbPackwriter {
            raw: out,
            progress,
            progress_payload_ptr,
        })
    }

    /// Checks if the object database has an object.
    pub fn exists(&self, oid: Oid) -> bool {
        unsafe { raw::git_odb_exists(self.raw, oid.raw()) != 0 }
    }

    /// Potentially finds an object that starts with the given prefix.
    pub fn exists_prefix(&self, short_oid: Oid, len: usize) -> Result<Oid, Error> {
        unsafe {
            let mut out = raw::git_oid {
                id: [0; raw::GIT_OID_RAWSZ],
            };
            try_call!(raw::git_odb_exists_prefix(
                &mut out,
                self.raw,
                short_oid.raw(),
                len
            ));
            Ok(Oid::from_raw(&out))
        }
    }

    /// Refresh the object database.
    /// This should never be needed, and is
    /// provided purely for convenience.
    /// The object database will automatically
    /// refresh when an object is not found when
    /// requested.
    pub fn refresh(&self) -> Result<(), Error> {
        unsafe {
            try_call!(raw::git_odb_refresh(self.raw));
            Ok(())
        }
    }

    /// Adds an alternate disk backend to the object database.
    pub fn add_disk_alternate(&self, path: &str) -> Result<(), Error> {
        unsafe {
            let path = CString::new(path)?;
            try_call!(raw::git_odb_add_disk_alternate(self.raw, path));
            Ok(())
        }
    }

    /// Create a new mempack backend, and add it to this odb with the given
    /// priority. Higher values give the backend higher precedence. The default
    /// loose and pack backends have priorities 1 and 2 respectively (hard-coded
    /// in libgit2). A reference to the new mempack backend is returned on
    /// success. The lifetime of the backend must be contained within the
    /// lifetime of this odb, since deletion of the odb will also result in
    /// deletion of the mempack backend.
    ///
    /// Here is an example that fails to compile because it tries to hold the
    /// mempack reference beyond the odb's lifetime:
    ///
    /// ```compile_fail
    /// use git2::Odb;
    /// let mempack = {
    ///     let odb = Odb::new().unwrap();
    ///     odb.add_new_mempack_backend(1000).unwrap()
    /// };
    /// ```
    pub fn add_new_mempack_backend<'odb>(
        &'odb self,
        priority: i32,
    ) -> Result<Mempack<'odb>, Error> {
        unsafe {
            let mut mempack = ptr::null_mut();
            // The mempack backend object in libgit2 is only ever freed by an
            // odb that has the backend in its list. So to avoid potentially
            // leaking the mempack backend, this API ensures that the backend
            // is added to the odb before returning it. The lifetime of the
            // mempack is also bound to the lifetime of the odb, so that users
            // can't end up with a dangling reference to a mempack object that
            // was actually freed when the odb was destroyed.
            try_call!(raw::git_mempack_new(&mut mempack));
            try_call!(raw::git_odb_add_backend(
                self.raw,
                mempack,
                priority as c_int
            ));
            Ok(Mempack::from_raw(mempack))
        }
    }
}

/// An object from the Object Database.
pub struct OdbObject<'a> {
    raw: *mut raw::git_odb_object,
    _marker: marker::PhantomData<Object<'a>>,
}

impl<'a> Binding for OdbObject<'a> {
    type Raw = *mut raw::git_odb_object;

    unsafe fn from_raw(raw: *mut raw::git_odb_object) -> OdbObject<'a> {
        OdbObject {
            raw: raw,
            _marker: marker::PhantomData,
        }
    }

    fn raw(&self) -> *mut raw::git_odb_object {
        self.raw
    }
}

impl<'a> Drop for OdbObject<'a> {
    fn drop(&mut self) {
        unsafe { raw::git_odb_object_free(self.raw) }
    }
}

impl<'a> OdbObject<'a> {
    /// Get the object type.
    pub fn kind(&self) -> ObjectType {
        unsafe { ObjectType::from_raw(raw::git_odb_object_type(self.raw)).unwrap() }
    }

    /// Get the object size.
    pub fn len(&self) -> usize {
        unsafe { raw::git_odb_object_size(self.raw) }
    }

    /// Get the object data.
    pub fn data(&self) -> &[u8] {
        unsafe {
            let size = self.len();
            let ptr: *const u8 = raw::git_odb_object_data(self.raw) as *const u8;
            let buffer = slice::from_raw_parts(ptr, size);
            return buffer;
        }
    }

    /// Get the object id.
    pub fn id(&self) -> Oid {
        unsafe { Oid::from_raw(raw::git_odb_object_id(self.raw)) }
    }
}

/// A structure to represent a git ODB rstream
pub struct OdbReader<'repo> {
    raw: *mut raw::git_odb_stream,
    _marker: marker::PhantomData<Object<'repo>>,
}

impl<'repo> Binding for OdbReader<'repo> {
    type Raw = *mut raw::git_odb_stream;

    unsafe fn from_raw(raw: *mut raw::git_odb_stream) -> OdbReader<'repo> {
        OdbReader {
            raw: raw,
            _marker: marker::PhantomData,
        }
    }
    fn raw(&self) -> *mut raw::git_odb_stream {
        self.raw
    }
}

impl<'repo> Drop for OdbReader<'repo> {
    fn drop(&mut self) {
        unsafe { raw::git_odb_stream_free(self.raw) }
    }
}

impl<'repo> io::Read for OdbReader<'repo> {
    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
        unsafe {
            let ptr = buf.as_ptr() as *mut c_char;
            let len = buf.len();
            let res = raw::git_odb_stream_read(self.raw, ptr, len);
            if res < 0 {
                Err(io::Error::new(io::ErrorKind::Other, "Read error"))
            } else {
                Ok(len)
            }
        }
    }
}

/// A structure to represent a git ODB wstream
pub struct OdbWriter<'repo> {
    raw: *mut raw::git_odb_stream,
    _marker: marker::PhantomData<Object<'repo>>,
}

impl<'repo> OdbWriter<'repo> {
    /// Finish writing to an ODB stream
    ///
    /// This method can be used to finalize writing object to the database and get an identifier.
    /// The object will take its final name and will be available to the odb.
    /// This method will fail if the total number of received bytes differs from the size declared with odb_writer()
    /// Attepting write after finishing will be ignored.
    pub fn finalize(&mut self) -> Result<Oid, Error> {
        let mut raw = raw::git_oid {
            id: [0; raw::GIT_OID_RAWSZ],
        };
        unsafe {
            try_call!(raw::git_odb_stream_finalize_write(&mut raw, self.raw));
            Ok(Binding::from_raw(&raw as *const _))
        }
    }
}

impl<'repo> Binding for OdbWriter<'repo> {
    type Raw = *mut raw::git_odb_stream;

    unsafe fn from_raw(raw: *mut raw::git_odb_stream) -> OdbWriter<'repo> {
        OdbWriter {
            raw: raw,
            _marker: marker::PhantomData,
        }
    }
    fn raw(&self) -> *mut raw::git_odb_stream {
        self.raw
    }
}

impl<'repo> Drop for OdbWriter<'repo> {
    fn drop(&mut self) {
        unsafe { raw::git_odb_stream_free(self.raw) }
    }
}

impl<'repo> io::Write for OdbWriter<'repo> {
    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
        unsafe {
            let ptr = buf.as_ptr() as *const c_char;
            let len = buf.len();
            let res = raw::git_odb_stream_write(self.raw, ptr, len);
            if res < 0 {
                Err(io::Error::new(io::ErrorKind::Other, "Write error"))
            } else {
                Ok(buf.len())
            }
        }
    }
    fn flush(&mut self) -> io::Result<()> {
        Ok(())
    }
}

struct OdbPackwriterCb<'repo> {
    cb: Option<Box<IndexerProgress<'repo>>>,
}

/// A stream to write a packfile to the ODB
pub struct OdbPackwriter<'repo> {
    raw: *mut raw::git_odb_writepack,
    progress: MaybeUninit<raw::git_indexer_progress>,
    progress_payload_ptr: *mut OdbPackwriterCb<'repo>,
}

impl<'repo> OdbPackwriter<'repo> {
    /// Finish writing the packfile
    pub fn commit(&mut self) -> Result<i32, Error> {
        unsafe {
            let writepack = &*self.raw;
            let res = match writepack.commit {
                Some(commit) => commit(self.raw, self.progress.as_mut_ptr()),
                None => -1,
            };

            if res < 0 {
                Err(Error::last_error(res).unwrap())
            } else {
                Ok(res)
            }
        }
    }

    /// The callback through which progress is monitored. Be aware that this is
    /// called inline, so performance may be affected.
    pub fn progress<F>(&mut self, cb: F) -> &mut OdbPackwriter<'repo>
    where
        F: FnMut(Progress<'_>) -> bool + 'repo,
    {
        let progress_payload =
            unsafe { &mut *(self.progress_payload_ptr as *mut OdbPackwriterCb<'_>) };

        progress_payload.cb = Some(Box::new(cb) as Box<IndexerProgress<'repo>>);
        self
    }
}

impl<'repo> io::Write for OdbPackwriter<'repo> {
    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
        unsafe {
            let ptr = buf.as_ptr() as *mut c_void;
            let len = buf.len();

            let writepack = &*self.raw;
            let res = match writepack.append {
                Some(append) => append(self.raw, ptr, len, self.progress.as_mut_ptr()),
                None => -1,
            };

            if res < 0 {
                Err(io::Error::new(io::ErrorKind::Other, "Write error"))
            } else {
                Ok(buf.len())
            }
        }
    }
    fn flush(&mut self) -> io::Result<()> {
        Ok(())
    }
}

impl<'repo> Drop for OdbPackwriter<'repo> {
    fn drop(&mut self) {
        unsafe {
            let writepack = &*self.raw;
            match writepack.free {
                Some(free) => free(self.raw),
                None => (),
            };

            Box::from_raw(self.progress_payload_ptr);
        }
    }
}

pub type ForeachCb<'a> = dyn FnMut(&Oid) -> bool + 'a;

struct ForeachCbData<'a> {
    pub callback: &'a mut ForeachCb<'a>,
}

extern "C" fn foreach_cb(id: *const raw::git_oid, payload: *mut c_void) -> c_int {
    panic::wrap(|| unsafe {
        let data = &mut *(payload as *mut ForeachCbData<'_>);
        let res = {
            let callback = &mut data.callback;
            callback(&Binding::from_raw(id))
        };

        if res {
            0
        } else {
            1
        }
    })
    .unwrap_or(1)
}

extern "C" fn write_pack_progress_cb(
    stats: *const raw::git_indexer_progress,
    payload: *mut c_void,
) -> c_int {
    let ok = panic::wrap(|| unsafe {
        let payload = &mut *(payload as *mut OdbPackwriterCb<'_>);

        let callback = match payload.cb {
            Some(ref mut cb) => cb,
            None => return true,
        };

        let progress: Progress<'_> = Binding::from_raw(stats);
        callback(progress)
    });
    if ok == Some(true) {
        0
    } else {
        -1
    }
}

#[cfg(test)]
mod tests {
    use crate::{Buf, ObjectType, Oid, Repository};
    use std::io::prelude::*;
    use tempfile::TempDir;

    #[test]
    fn read() {
        let td = TempDir::new().unwrap();
        let repo = Repository::init(td.path()).unwrap();
        let dat = [4, 3, 5, 6, 9];
        let id = repo.blob(&dat).unwrap();
        let db = repo.odb().unwrap();
        let obj = db.read(id).unwrap();
        let data = obj.data();
        let size = obj.len();
        assert_eq!(size, 5);
        assert_eq!(dat, data);
        assert_eq!(id, obj.id());
    }

    #[test]
    fn read_header() {
        let td = TempDir::new().unwrap();
        let repo = Repository::init(td.path()).unwrap();
        let dat = [4, 3, 5, 6, 9];
        let id = repo.blob(&dat).unwrap();
        let db = repo.odb().unwrap();
        let (size, kind) = db.read_header(id).unwrap();

        assert_eq!(size, 5);
        assert_eq!(kind, ObjectType::Blob);
    }

    #[test]
    fn write() {
        let td = TempDir::new().unwrap();
        let repo = Repository::init(td.path()).unwrap();
        let dat = [4, 3, 5, 6, 9];
        let db = repo.odb().unwrap();
        let id = db.write(ObjectType::Blob, &dat).unwrap();
        let blob = repo.find_blob(id).unwrap();
        assert_eq!(blob.content(), dat);
    }

    #[test]
    fn writer() {
        let td = TempDir::new().unwrap();
        let repo = Repository::init(td.path()).unwrap();
        let dat = [4, 3, 5, 6, 9];
        let db = repo.odb().unwrap();
        let mut ws = db.writer(dat.len(), ObjectType::Blob).unwrap();
        let wl = ws.write(&dat[0..3]).unwrap();
        assert_eq!(wl, 3);
        let wl = ws.write(&dat[3..5]).unwrap();
        assert_eq!(wl, 2);
        let id = ws.finalize().unwrap();
        let blob = repo.find_blob(id).unwrap();
        assert_eq!(blob.content(), dat);
    }

    #[test]
    fn exists() {
        let td = TempDir::new().unwrap();
        let repo = Repository::init(td.path()).unwrap();
        let dat = [4, 3, 5, 6, 9];
        let db = repo.odb().unwrap();
        let id = db.write(ObjectType::Blob, &dat).unwrap();
        assert!(db.exists(id));
    }

    #[test]
    fn exists_prefix() {
        let td = TempDir::new().unwrap();
        let repo = Repository::init(td.path()).unwrap();
        let dat = [4, 3, 5, 6, 9];
        let db = repo.odb().unwrap();
        let id = db.write(ObjectType::Blob, &dat).unwrap();
        let id_prefix_str = &id.to_string()[0..10];
        let id_prefix = Oid::from_str(id_prefix_str).unwrap();
        let found_oid = db.exists_prefix(id_prefix, 10).unwrap();
        assert_eq!(found_oid, id);
    }

    #[test]
    fn packwriter() {
        let (_td, repo_source) = crate::test::repo_init();
        let (_td, repo_target) = crate::test::repo_init();
        let mut builder = t!(repo_source.packbuilder());
        let mut buf = Buf::new();
        let (commit_source_id, _tree) = crate::test::commit(&repo_source);
        t!(builder.insert_object(commit_source_id, None));
        t!(builder.write_buf(&mut buf));
        let db = repo_target.odb().unwrap();
        let mut packwriter = db.packwriter().unwrap();
        packwriter.write(&buf).unwrap();
        packwriter.commit().unwrap();
        let commit_target = repo_target.find_commit(commit_source_id).unwrap();
        assert_eq!(commit_target.id(), commit_source_id);
    }

    #[test]
    fn packwriter_progress() {
        let mut progress_called = false;
        {
            let (_td, repo_source) = crate::test::repo_init();
            let (_td, repo_target) = crate::test::repo_init();
            let mut builder = t!(repo_source.packbuilder());
            let mut buf = Buf::new();
            let (commit_source_id, _tree) = crate::test::commit(&repo_source);
            t!(builder.insert_object(commit_source_id, None));
            t!(builder.write_buf(&mut buf));
            let db = repo_target.odb().unwrap();
            let mut packwriter = db.packwriter().unwrap();
            packwriter.progress(|_| {
                progress_called = true;
                true
            });
            packwriter.write(&buf).unwrap();
            packwriter.commit().unwrap();
        }
        assert_eq!(progress_called, true);
    }

    #[test]
    fn write_with_mempack() {
        use crate::{Buf, ResetType};
        use std::io::Write;
        use std::path::Path;

        // Create a repo, add a mempack backend
        let (_td, repo) = crate::test::repo_init();
        let odb = repo.odb().unwrap();
        let mempack = odb.add_new_mempack_backend(1000).unwrap();

        // Sanity check that foo doesn't exist initially
        let foo_file = Path::new(repo.workdir().unwrap()).join("foo");
        assert!(!foo_file.exists());

        // Make a commit that adds foo. This writes new stuff into the mempack
        // backend.
        let (oid1, _id) = crate::test::commit(&repo);
        let commit1 = repo.find_commit(oid1).unwrap();
        t!(repo.reset(commit1.as_object(), ResetType::Hard, None));
        assert!(foo_file.exists());

        // Dump the mempack modifications into a buf, and reset it. This "erases"
        // commit-related objects from the repository. Ensure the commit appears
        // to have become invalid, by checking for failure in `reset --hard`.
        let mut buf = Buf::new();
        mempack.dump(&repo, &mut buf).unwrap();
        mempack.reset().unwrap();
        assert!(repo
            .reset(commit1.as_object(), ResetType::Hard, None)
            .is_err());

        // Write the buf into a packfile in the repo. This brings back the
        // missing objects, and we verify everything is good again.
        let mut packwriter = odb.packwriter().unwrap();
        packwriter.write(&buf).unwrap();
        packwriter.commit().unwrap();
        t!(repo.reset(commit1.as_object(), ResetType::Hard, None));
        assert!(foo_file.exists());
    }
}
