use std::path::Path;
use std::ptr;
use libc::{c_int, c_void};

use {raw, Blob, Buf, Diff, DiffDelta, DiffHunk, DiffLine, DiffOptions, Error};
use diff::{LineCb, print_cb};
use util::{Binding, into_opt_c_string};

/// A structure representing the text changes in a single diff delta.
///
/// This is an opaque structure.
pub struct Patch {
    raw: *mut raw::git_patch,
}

unsafe impl Send for Patch {}

impl Binding for Patch {
    type Raw = *mut raw::git_patch;
    unsafe fn from_raw(raw: Self::Raw) -> Patch {
        Patch { raw: raw }
    }
    fn raw(&self) -> Self::Raw { self.raw }
}

impl Drop for Patch {
    fn drop(&mut self) {
        unsafe { raw::git_patch_free(self.raw) }
    }
}

impl Patch {
    /// Return a Patch for one file in a Diff.
    ///
    /// Returns Ok(None) for an unchanged or binary file.
    pub fn from_diff(diff: &Diff, idx: usize) -> Result<Option<Patch>, Error> {
        let mut ret = ptr::null_mut();
        unsafe {
            try_call!(raw::git_patch_from_diff(&mut ret, diff.raw(), idx));
            Ok(Binding::from_raw_opt(ret))
        }
    }

    /// Generate a Patch by diffing two blobs.
    pub fn from_blobs(old_blob: &Blob,
                      old_path: Option<&Path>,
                      new_blob: &Blob,
                      new_path: Option<&Path>,
                      opts: Option<&mut DiffOptions>)
                      -> Result<Patch, Error>
    {
        let mut ret = ptr::null_mut();
        let old_path = try!(into_opt_c_string(old_path));
        let new_path = try!(into_opt_c_string(new_path));
        unsafe {
            try_call!(raw::git_patch_from_blobs(&mut ret,
                                                old_blob.raw(),
                                                old_path,
                                                new_blob.raw(),
                                                new_path,
                                                opts.map(|s| s.raw())));
            Ok(Binding::from_raw(ret))
        }
    }

    /// Generate a Patch by diffing a blob and a buffer.
    pub fn from_blob_and_buffer(old_blob: &Blob,
                                old_path: Option<&Path>,
                                new_buffer: &[u8],
                                new_path: Option<&Path>,
                                opts: Option<&mut DiffOptions>)
                                -> Result<Patch, Error>
    {
        let mut ret = ptr::null_mut();
        let old_path = try!(into_opt_c_string(old_path));
        let new_path = try!(into_opt_c_string(new_path));
        unsafe {
            try_call!(raw::git_patch_from_blob_and_buffer(&mut ret,
                                                          old_blob.raw(),
                                                          old_path,
                                                          new_buffer.as_ptr() as *const c_void,
                                                          new_buffer.len(),
                                                          new_path,
                                                          opts.map(|s| s.raw())));
            Ok(Binding::from_raw(ret))
        }
    }

    /// Generate a Patch by diffing two buffers.
    pub fn from_buffers(old_buffer: &[u8],
                        old_path: Option<&Path>,
                        new_buffer: &[u8],
                        new_path: Option<&Path>,
                        opts: Option<&mut DiffOptions>)
                        -> Result<Patch, Error>
    {
        let mut ret = ptr::null_mut();
        let old_path = try!(into_opt_c_string(old_path));
        let new_path = try!(into_opt_c_string(new_path));
        unsafe {
            try_call!(raw::git_patch_from_buffers(&mut ret,
                                                 old_buffer.as_ptr() as *const c_void,
                                                 old_buffer.len(),
                                                 old_path,
                                                 new_buffer.as_ptr() as *const c_void,
                                                 new_buffer.len(),
                                                 new_path,
                                                 opts.map(|s| s.raw())));
            Ok(Binding::from_raw(ret))
        }
    }

    /// Get the DiffDelta associated with the Patch.
    pub fn delta(&self) -> DiffDelta {
        unsafe {
            Binding::from_raw(raw::git_patch_get_delta(self.raw) as *mut _)
        }
    }

    /// Get the number of hunks in the Patch.
    pub fn num_hunks(&self) -> usize {
        unsafe {
            raw::git_patch_num_hunks(self.raw)
        }
    }

    /// Get the number of lines of context, additions, and deletions in the Patch.
    pub fn line_stats(&self) -> Result<(usize, usize, usize), Error> {
        let mut context = 0;
        let mut additions = 0;
        let mut deletions = 0;
        unsafe {
            try_call!(raw::git_patch_line_stats(&mut context,
                                                &mut additions,
                                                &mut deletions,
                                                self.raw));
        }
        Ok((context, additions, deletions))
    }

    /// Get a DiffHunk and its total line count from the Patch.
    pub fn hunk(&self, hunk_idx: usize) -> Result<(DiffHunk, usize), Error> {
        let mut ret = ptr::null();
        let mut lines = 0;
        unsafe {
            try_call!(raw::git_patch_get_hunk(&mut ret, &mut lines, self.raw, hunk_idx));
            Ok((Binding::from_raw(ret), lines))
        }
    }

    /// Get the number of lines in a hunk.
    pub fn num_lines_in_hunk(&self, hunk_idx: usize) -> Result<usize, Error> {
        unsafe {
            Ok(try_call!(raw::git_patch_num_lines_in_hunk(self.raw, hunk_idx)) as usize)
        }
    }

    /// Get a DiffLine from a hunk of the Patch.
    pub fn line_in_hunk(&self,
                        hunk_idx: usize,
                        line_of_hunk: usize) -> Result<DiffLine, Error> {
        let mut ret = ptr::null();
        unsafe {
            try_call!(raw::git_patch_get_line_in_hunk(&mut ret,
                                                      self.raw,
                                                      hunk_idx,
                                                      line_of_hunk));
            Ok(Binding::from_raw(ret))
        }
    }

    /// Get the size of a Patch's diff data in bytes.
    pub fn size(&self,
                include_context: bool,
                include_hunk_headers: bool,
                include_file_headers: bool) -> usize {
        unsafe {
            raw::git_patch_size(self.raw,
                                include_context as c_int,
                                include_hunk_headers as c_int,
                                include_file_headers as c_int)
        }
    }

    /// Print the Patch to text via a callback.
    pub fn print(&mut self, mut line_cb: &mut LineCb) -> Result<(), Error> {
        let ptr = &mut line_cb as *mut _ as *mut c_void;
        unsafe {
            try_call!(raw::git_patch_print(self.raw, print_cb, ptr));
            Ok(())
        }
    }

    /// Get the Patch text as a Buf.
    pub fn to_buf(&mut self) -> Result<Buf, Error> {
        let buf = Buf::new();
        unsafe {
            try_call!(raw::git_patch_to_buf(buf.raw(), self.raw));
        }
        Ok(buf)
    }
}
