use bstr::ByteSlice;
use format_bytes::format_bytes;
use std::{
    borrow::Cow, cell::OnceCell, cmp::Ordering, env, fs, fs::File, io, ops::Range,
    str::FromStr, sync::Arc
};
use std::ffi::{OsStr, OsString};
use std::io::{BufRead, BufReader, Write};
use std::os::unix::ffi::{OsStrExt, OsStringExt};
use std::path::{Path, PathBuf};
use std::process::{Child, Command, Stdio};
use super::{diff_meta, lines::Lines, lines::TypedLines};

#[derive(Clone, Debug)]
pub struct Git {
    path: Arc<OsStr>,
    bare: bool,
}

pub struct Walk<'a> {
    git: &'a Git,
    rev: &'a [u8],
    base: Option<Vec<u8>>,
    file: Option<&'a [u8]>,
    line_range: Option<Vec<u8>>,
}

#[derive(Clone, derive_more::Debug)]
pub struct Description {
    #[debug("{:?}", id.as_bstr())]
    pub id: Vec<u8>,
    #[debug("{:?}", author.as_bstr())]
    pub author: Vec<u8>,
    #[debug("{:?}", author_date.as_bstr())]
    pub author_date: Vec<u8>,
    pub author_timestamp: i64,
    pub commit_timestamp: i64,
    #[debug("{:?}", subject.as_bstr())]
    pub subject: Vec<u8>,
    #[debug("{:?}", body.as_bstr())]
    pub body: Vec<u8>,
    diff_lines: OnceCell<Lines>,
}

impl Git {
    pub fn open(path: &OsStr) -> io::Result<Git> {
        let bare = Self::is_bare(path)?;
        let real_path = if bare {
            fs::canonicalize(Path::new(&path))?.into_os_string()
        } else {
            OsString::from_vec(Self::get_toplevel(path)?)
        };
        Ok(Git {
            path: real_path.into(),
            bare,
        })
    }

    /// Initialize a new empty repository at the given path, which must be an existing
    /// directory. There's currently no check there's not a repository already there;
    /// the result in such case is most likely not what you wanted.
    pub fn create(path: &OsStr, bare: bool) -> io::Result<Git> {
        Self::init(path, bare)?;
        Self::open(path)
    }

    fn exec_check(path: &OsStr, args: &[&[u8]], success: fn(i32) -> bool)
            -> io::Result<Vec<u8>> {
        let output = Command::new("git")
            .current_dir(path)
            .args(args.into_iter().map(|x| OsStr::from_bytes(x)))
            .output()?;
        match output.status.code() {
            Some(code) if success(code) => Ok(output.stdout),
            _ => Err(io::Error::new(
                    io::ErrorKind::Other,
                    if output.stderr != b"" {
                        output.stderr.to_str_lossy()
                    } else if output.stdout != b"" {
                        output.stdout.to_str_lossy()
                    } else {
                        Cow::Borrowed("Error executing git")
                    }
                ))
        }
    }

    fn exec(&self, args: &[&[u8]]) -> io::Result<Vec<u8>> {
        Self::exec_check(&self.path, args, |code| code == 0)
    }

    fn exec_iter(&self, args: &[&[u8]], separator: u8) -> GitOutputIterator
    {
        let child = Command::new("git")
            .current_dir(&*self.path)
            .stdin(Stdio::null())
            .stderr(Stdio::null())
            .stdout(Stdio::piped())
            .args(args.into_iter().map(|x| OsStr::from_bytes(x)))
            .spawn();
        GitOutputIterator::new(child, separator)
    }

    fn is_bare(path: &OsStr) -> io::Result<bool> {
        let answer = Self::exec_check(
            path,
            &[
                b"rev-parse",
                b"--is-bare-repository",
            ],
            |code| code == 0,
        )
            .map(|s| s.rstrip(b'\n'))?;
        if answer == b"true" {
            Ok(true)
        } else if answer == b"false" {
            Ok(false)
        } else {
            Err(io::Error::new(
                io::ErrorKind::Other,
                "Invalid output from git rev-parse --is-bare-repository"
            ))
        }

    }

    fn get_toplevel(path: &OsStr) -> io::Result<Vec<u8>> {
        Self::exec_check(
            path,
            &[
                b"rev-parse",
                b"--path-format=absolute",
                b"--show-toplevel",
            ],
            |code| code == 0,
        )
            .map(|s| s.rstrip(b'\n'))
    }

    fn init(path: &OsStr, bare: bool) -> io::Result<()> {
        let mut args: Vec<&[u8]> = vec![
            b"init",
            b"--quiet",
        ];
        if bare {
            args.push(b"--bare");
        }
        Self::exec_check(path, &args, |code| code == 0)
            .map(|_| ())
    }

    /// Change to the top level of the working tree.
    pub fn chdir(&self) -> io::Result<()> {
        if self.bare {
            Err(io::Error::new(io::ErrorKind::Other, "Cannot change to a bare repository"))
        } else {
            env::set_current_dir(&*self.path)
        }
    }

    /// Get the current branch or None if the current HEAD is not on any branch.
    pub fn current_branch(&self) -> io::Result<Option<Vec<u8>>> {
        let branch = self.exec(&[
            b"branch",
            b"--show-current",
        ])?
            .rstrip(b'\n');
        if branch.is_empty() {
            Ok(None)
        } else {
            Ok(Some(branch))
        }
    }

    pub fn walk<'a>(&'a self, rev: &'a [u8]) -> Walk<'a> {
        Walk {
            git: self,
            rev,
            base: None,
            file: None,
            line_range: None,
        }
    }

    pub fn revparse(&self, rev: &[u8]) -> io::Result<Vec<u8>> {
        self.exec(&[
            b"rev-parse",
            b"--verify",
            b"--end-of-options",
            &format_bytes!(b"{}^{}object}", rev, b"{"),
        ])
            .map(|s| s.rstrip(b'\n'))
    }

    pub fn description(&self, rev: &[u8]) -> io::Result<Description> {
        let mut data = self.exec_iter(&[
            b"log",
            b"-1",
            b"--pretty=tformat:%H%x00%an <%ae>%x00%ad%x00%at%x00%ct%x00%s%x00%b",
            b"--end-of-options",
            rev,
            b"--",
        ], 0);
        let err = || io::Error::new(io::ErrorKind::Other, "Invalid output from git log");
        Ok(Description {
            id: data.next().transpose()?.ok_or_else(err)?,
            author: data.next().transpose()?.ok_or_else(err)?,
            author_date: data.next().transpose()?.ok_or_else(err)?,
            author_timestamp: data.next().transpose()?
                .and_then(|t| t.bytes_parse()).ok_or_else(err)?,
            commit_timestamp: data.next().transpose()?
                .and_then(|t| t.bytes_parse()).ok_or_else(err)?,
            subject: data.next().transpose()?.ok_or_else(err)?,
            body: data.next().transpose()?.ok_or_else(err)?,
            diff_lines: OnceCell::new(),
        })
    }

    pub fn diff(&self, rev: &[u8]) -> io::Result<Lines> {
        self.exec(&[
            b"show",
            b"--pretty=",
            b"--end-of-options",
            rev,
            b"--",
        ]).map(|data| data.into())
    }

    fn parse_diff_change<'a>(&self, change: &'a [u8])
        -> Option<(diff_meta::FileMode, diff_meta::FileMode, Vec<u8>, Vec<u8>, u8)>
    {
        if change.len() < 2 || change[0] != b':' {
            return None;
        }
        let mut fields = change[1 ..].split(|&b| b == b' ');
        let mode_src = diff_meta::FileMode::from_ascii(fields.next()?)?;
        let mode_dst = diff_meta::FileMode::from_ascii(fields.next()?)?;
        let sha_src = fields.next()?;
        let sha_dst = fields.next()?;
        if sha_src.is_empty() ||
            sha_src.iter().any(|b| !b.is_ascii_hexdigit()) ||
            sha_dst.is_empty() ||
            sha_dst.iter().any(|b| !b.is_ascii_hexdigit())
        {
            return None;
        }
        let status = fields.next()?;
        if status.len() == 0 || !b"ACDMRT".contains(&status[0]) {
            return None;
        }
        Some((mode_src, mode_dst, sha_src.to_vec(), sha_dst.to_vec(), status[0]))
    }

    /// Parse the raw diff into Metadata. Return Ok(None) when we've reached end of files.
    /// Otherwise, return Ok((metadata, sha_src, sha_dest)).
    /// If anything goes wrong, return Err.
    fn parts_to_diff_file_metadata<'a>(
        &self,
        mut parts: impl Iterator<Item = io::Result<Vec<u8>>>
    ) -> io::Result<Option<(diff_meta::Metadata, Vec<u8>, Vec<u8>)>>
    {
        let change = match parts.next()
            .transpose()?
            // Ignore newline at the beginning. Git insists on putting it there whenever
            // we pass a non-empty --pretty.
            .map(|mut v| {
                if v.starts_with(b"\n") {
                    v.remove(0);
                }
                v
            })
            .and_then(|v| if v == b"" { None } else { Some(v) })
        {
            Some(change) => change,
            // no more files:
            None => return Ok(None)
        };
        let change = self.parse_diff_change(&change)
            .ok_or_else(|| io::Error::new(
                io::ErrorKind::Other,
                "Unexpected file change description while parsing git log --raw"
            ))?;
        let name = parts.next()
            .transpose()?
            .ok_or_else(|| io::Error::new(
                io::ErrorKind::Other,
                "Missing file name while parsing git log --raw"
            ))?;
        let err_dest = || io::Error::new(
            io::ErrorKind::Other,
            "Missing destination file name while parsing git log --raw",
        );
        let metadata = match change.4 {
            b'A' => diff_meta::Metadata::new(name, change.1, diff_meta::Op::Added),
            // We currently don't track copies. Treat them as a new file.
            b'C' => diff_meta::Metadata::new(
                parts.next().transpose()?.ok_or_else(err_dest)?,
                change.1,
                diff_meta::Op::Added
            ),
            b'D' => diff_meta::Metadata::new(name, change.0, diff_meta::Op::Removed),
            b'M' | b'T' => diff_meta::Metadata::new(
                name,
                change.0,
                diff_meta::Op::Modified {
                    new_name: None,
                    new_mode: change.1,
                },
            ),
            b'R' => diff_meta::Metadata::new(
                name,
                change.0,
                diff_meta::Op::Modified {
                    new_name: Some(parts.next().transpose()?.ok_or_else(err_dest)?),
                    new_mode: change.1,
                },
            ),
            // We can't encounter anything else, since we checked the possible values in
            // parse_diff_change.
            _ => panic!(),
        };
        Ok(Some((metadata, change.2, change.3)))
    }

    /// Parse the raw diff into FileChange. Return Ok(None) when we've reached end of files.
    /// Otherwise, return Ok(file_change).
    /// If anything goes wrong, return Err.
    fn parts_to_diff_file_change(&self, parts: impl Iterator<Item = io::Result<Vec<u8>>>)
        -> io::Result<Option<diff_meta::FileChange>>
    {
        let (metadata, sha_src, sha_dest) = match self.parts_to_diff_file_metadata(parts)? {
            Some(result) => result,
            None => return Ok(None),
        };
        let src = TypedLines::from_vec_and_name(self.blob(&sha_src)?, &metadata.name);
        let dest = self.blob(&sha_dest)?.into();
        Ok(Some(diff_meta::FileChange::new(metadata, src, dest)))
    }

    fn parse_status_unmerged<'a>(&self, status: &'a [u8])
        -> Option<(
            [u8;2],                 // XY
            diff_meta::FileMode,    // base mode
            diff_meta::FileMode,    // theirs mode
            Vec<u8>,                // file sha ours
            Vec<u8>,                // file sha base
            Vec<u8>,                // file sha theirs
            Vec<u8>,                // path
        )>
    {
        let mut fields = status.splitn(11, |&b| b == b' ');
        let fmt = fields.next()?;
        if fmt != b"u" {
            return None;
        }
        let xy = fields.next()?;
        if xy.len() != 2 {
            return None;
        }
        let xy = xy.try_into().unwrap();
        fields.next()?; // skip the submodule state
        let mode_before = diff_meta::FileMode::from_ascii(fields.next()?)?;
        fields.next()?; // skip the "ours" file mode
        let mode_after = diff_meta::FileMode::from_ascii(fields.next()?)?;
        fields.next()?; // skip the worktree file mode
        let sha_before = fields.next()?.to_vec();
        let sha_current = fields.next()?.to_vec();
        let sha_after = fields.next()?.to_vec();
        let path = fields.next()?.to_vec();
        Some((xy, mode_before, mode_after, sha_current, sha_before, sha_after, path))
    }

    /// Parse the porcelain v2 status into a tuple of blob sha, FileChange. Return Ok(None)
    /// when we've reached end of files. Otherwise, return Ok((destination, source)).
    /// If anything goes wrong, return Err.
    fn parts_to_status_unmerged(&self, mut parts: impl Iterator<Item = io::Result<Vec<u8>>>)
        -> io::Result<Option<(Vec<u8>, diff_meta::FileChange)>>
    {
        let data = loop {
            let status = match parts.next().transpose()? {
                Some(status) => status,
                None => return Ok(None)
            };
            if let Some(data) = self.parse_status_unmerged(&status) {
                break data;
            }
        };
        let (xy, mode_before, mode_after, sha_current, sha_before, sha_after, path) = data;
        let op = match &xy {
            b"UU" | b"DU" => diff_meta::Op::Modified {
                new_name: None,
                new_mode: if mode_before == mode_after {
                    mode_after
                } else {
                    diff_meta::FileMode::default()
                },
            },
            b"AA" => diff_meta::Op::Added,
            b"UD" => diff_meta::Op::Removed,
            _ => return Err(io::Error::new(io::ErrorKind::Other, format!(
                "Unexpected file status for {}: \"{}\"", path.as_bstr(), xy.as_bstr()
            )))
        };
        let before = TypedLines::from_vec_and_name(self.blob(&sha_before)?, &path);
        let after = self.blob(&sha_after)?.into();
        let metadata = diff_meta::Metadata::new(path, mode_before, op);
        Ok(Some((sha_current, diff_meta::FileChange::new(metadata, before, after))))
    }

    pub fn merge_base(&self, rev1: &[u8], rev2: &[u8]) -> io::Result<Vec<u8>> {
        self.exec(&[
            b"merge-base",
            b"--end-of-options",
            rev1,
            rev2
        ])
            .map(|s| s.rstrip(b'\n'))
    }

    pub fn raw_diff(&self, rev: &[u8]) -> io::Result<Vec<diff_meta::FileChange>> {
        let mut data = self.exec_iter(&[
            b"-c", b"diff.renames=true",
            b"log",
            b"-1",
            b"--raw",
            b"--pretty=",
            b"-z",
            b"--end-of-options",
            rev,
            b"--",
        ], 0);
        let mut result = Vec::new();
        loop {
            if let Some(item) = self.parts_to_diff_file_change(&mut data)? {
                result.push(item);
            } else {
                break;
            }
        }
        Ok(result)
    }

    pub(crate) fn blob(&self, rev: &[u8]) -> io::Result<Vec<u8>> {
        if rev.iter().all(|&b| b == b'0') {
            return Ok(Vec::new());
        }
        self.exec(&[
            b"cat-file",
            b"blob",
            b"--end-of-options",
            rev,
        ])
    }

    pub fn file(&self, rev: &[u8], path: &[u8]) -> io::Result<Vec<u8>> {
        self.exec(&[
            b"show",
            b"--end-of-options",
            &format_bytes!(b"{}:{}", rev, path),
            b"--",
        ])
    }

    /// Return the previous names of the given `path` in the range `base..rev` (or `..rev`
    /// if `base` is None). The names are sorted from the newest to the oldest. The current
    /// name (`path`) is not included. If the file had the same name several times in the
    /// history, that returned vec will reflect that (e.g. given this chain of renames:
    /// `a` -> `b` -> `a` -> `c` -> `d`, the vec `[b"c", "b"a", b"b", b"a"]` will be returned
    /// for `d`).
    pub fn previous_paths(&self, rev: &[u8], base: Option<&[u8]>, path: &[u8])
        -> io::Result<Vec<Vec<u8>>>
    {
        let mut names = Vec::new();
        let rev = match base {
            Some(base) => &format_bytes!(b"{}..{}", base, rev),
            None => rev,
        };
        let mut data = self.exec_iter(&[
            b"log",
            b"--follow",
            b"--raw",
            b"--pretty=",
            b"-z",
            b"--end-of-options",
            rev,
            b"--",
            path,
        ], 0);
        loop {
            let metadata = match self.parts_to_diff_file_metadata(&mut data)? {
                Some((metadata, _, _)) => metadata,
                None => break,
            };
            if matches!(metadata.op, diff_meta::Op::Modified { .. }) {
                if (names.is_empty() && path != metadata.name) ||
                    (!names.is_empty() && names[names.len() - 1] != metadata.name)
                {
                    names.push(metadata.name);
                }
            }
        }
        Ok(names)
    }

    /// Find to what the given `path` got renamed last in the range `base..rev` (or `..rev`
    /// if `base` is None). Return `Some((new_path, commit_id))` when successful. The
    /// `commit_id` contains the id of the commit that caused the rename; it can be used
    /// to continue the search. Return `None` if the file was not renamed.
    fn next_future_path(&self, rev: &[u8], base: Option<&[u8]>, path: &[u8])
        -> io::Result<Option<(Vec<u8>, Vec<u8>)>>
    {
        let rev = match base {
            Some(base) => &format_bytes!(b"{}..{}", base, rev),
            None => rev,
        };
        let mut data = self.exec_iter(&[
            b"-c", b"diff.renames=true",
            b"log",
            b"-1",
            b"--raw",
            b"--full-diff",
            b"--pretty=tformat:%H",
            b"-z",
            b"--end-of-options",
            rev,
            b"--",
            path,
        ], 0);
        let commit = match data.next().transpose()? {
            Some(commit) => commit,
            None => return Ok(None),
        };
        loop {
            let metadata = match self.parts_to_diff_file_metadata(&mut data)? {
                Some((metadata, _, _)) => metadata,
                None => return Ok(None),
            };
            if metadata.name == path {
                match metadata.op {
                    diff_meta::Op::Modified { new_name: Some(new_name), .. } =>
                        return Ok(Some((new_name, commit.to_vec()))),
                    diff_meta::Op::Removed =>
                        // We're on the wrong track. This was a different file.
                        return Ok(None),
                    _ => ()
                }
            }
        }
    }

    /// Given a historical `path`, find what the current path of that file is. Search only
    /// in the range `base..rev` (or `..rev` if `base` is None). Return None if the file
    /// did not get renamed.
    pub fn future_path(&self, rev: &[u8], base: Option<&[u8]>, path: &[u8])
        -> io::Result<Option<Vec<u8>>>
    {
        let mut base = base.map(|v| v.to_vec());
        let mut current = path.to_vec();
        while let Some((new_path, commit_id)) =
            self.next_future_path(rev, base.as_deref(), &current)?
        {
            current = new_path;
            base = Some(commit_id);
        }
        if &current != path {
            Ok(Some(current))
        } else {
            Ok(None)
        }
    }

    /// Get the merge conflicts. For each conflicting file, return the tuple of the target
    /// blob sha (the "ours" side) and of the source `FileChange` (the "theirs" side).
    pub(crate) fn conflicts(&self) -> io::Result<Vec<(Vec<u8>, diff_meta::FileChange)>> {
        let mut data = self.exec_iter(&[
            b"status",
            b"--porcelain=2",
            b"-z",
            b"-uno"
        ], 0);
        let mut result = Vec::new();
        loop {
            if let Some(item) = self.parts_to_status_unmerged(&mut data)? {
                result.push(item);
            } else {
                break;
            }
        }
        Ok(result)
    }

    pub fn is_clean(&self) -> io::Result<bool> {
        Ok(self.exec(&[
            b"status",
            b"--porcelain=1",
            b"-z",
            b"-uno"
        ])?.is_empty())
    }

    /// Is there anything staged for commit?
    pub fn is_staged(&self) -> io::Result<bool> {
        Ok(self.exec(&[
            b"status",
            b"--porcelain=1",
            b"-z",
            b"-uno"
        ])?
            .split(|&b| b == b'\0')
            .any(|b| b.len() > 0 && !b.starts_with(b" ")))
    }

    pub fn add_to_index<'a>(&self, it: impl IntoIterator<Item = &'a OsStr>)
            -> io::Result<()> {
        let mut args: Vec<&[u8]> = vec![b"add", b"--"];
        args.extend(it.into_iter().map(|path| path.as_bytes()));
        self.exec(args.as_slice()).map(|_| ())
    }

    pub fn commit_index(&self, message: &[u8]) -> io::Result<()> {
        self.exec(&[
            b"commit",
            b"-s",
            b"-m", message,
        ]).map(|_| ())
    }

    pub fn add_to_alternates<'a>(&self, repos: impl IntoIterator<Item = &'a Git>)
        -> io::Result<()>
    {
        let mut dest = PathBuf::from(&self.path);
        if !self.bare {
            dest.push(".git");
        }
        dest.push("objects/info/alternates");
        let mut file = File::options().append(true).create(true).open(&dest)?;
        for repo in repos {
            file.write_all(repo.path.as_bytes())?;
            if !repo.bare {
                file.write_all(b"/.git")?;
            }
            file.write_all(b"/objects\n")?;
        }
        Ok(())
    }
}

impl PartialEq for Git {
    fn eq(&self, other: &Self) -> bool {
        self.path == other.path
    }
}

impl<'a> Walk<'a> {
    /// Set the base commit. That is, the commit where the walk will stop. The commit will
    /// not be included.
    pub fn base(mut self, base: Option<&'a [u8]>) -> Self {
        self.base = base.map(|b| format_bytes!(b"^{}", b));
        self
    }

    pub(crate) fn file(mut self, file: Option<&'a [u8]>) -> Self {
        self.file = file;
        self
    }

    /// Trace the evolution of the given line range in the given file. The line range has
    /// the usual Rust semantics, i.e. the first line is 0 and the right range boundary is
    /// exclusive. It's invalid to call both `line_range()` and `file()`.
    pub(crate) fn line_range(mut self, file: &'a [u8], range: &Range<usize>) -> Self {
        self.line_range = Some(format_bytes!(
            b"-L{},{}:{}",
            &(range.start + 1),
            &range.end,
            file,
        ));
        self
    }

    fn create_args(&self, format: &'static [u8]) -> Vec<&[u8]> {
        let mut args: Vec<&[u8]> = vec![
            b"log",
            b"--no-merges",
            b"--no-patch",
            b"-z",
            format,
        ];
        if self.file.is_some() {
            args.push(b"--follow");
        }
        if let Some(line_range) = &self.line_range {
            assert!(self.file.is_none());
            args.push(line_range);
        }
        args.push(b"--end-of-options");
        args.push(self.rev);
        if let Some(base) = &self.base {
            args.push(base);
        }
        args.push(b"--");
        if let Some(file) = self.file {
            args.push(file);
        }
        args
    }

    /// Iterates over shas.
    pub(crate) fn iter_sha(&self) -> impl Iterator<Item = io::Result<Vec<u8>>> {
        self.git.exec_iter(&self.create_args(b"--pretty=tformat:%H"), 0)
    }

    /// Iterates over tuples of (sha, body).
    pub(crate) fn iter_sha_body(&self)
        -> impl Iterator<Item = io::Result<(Vec<u8>, Vec<u8>)>>
    {
        self.git.exec_iter(&self.create_args(b"--pretty=tformat:%H %B"), 0)
            .map(|item| match item {
                Ok(mut buf) => {
                    if let Some(index) = buf.iter().position(|&b| b == b' ') {
                        let mut hash: Vec<u8> = buf.drain(0 .. index + 1).collect();
                        hash.truncate(index);
                        Ok((hash, buf))
                    } else {
                        Err(io::Error::new(
                            io::ErrorKind::Other,
                            "Invalid output from git log"
                        ))
                    }
                }
                Err(err) => Err(err),
            })
    }
}

struct GitOutputIterator {
    child: Option<Child>,
    field_iter: Option<io::Result<io::Split<BufReader<std::process::ChildStdout>>>>,
}

impl GitOutputIterator {
    fn new(child: io::Result<Child>, separator: u8) -> GitOutputIterator {
        match child {
            Ok(mut child) => {
                let reader = BufReader::new(child.stdout.take().unwrap());
                GitOutputIterator {
                    child: Some(child),
                    field_iter: Some(Ok(reader.split(separator))),
                }
            }
            Err(err) => GitOutputIterator {
                child: None,
                field_iter: Some(Err(err)),
            }
        }
    }

    fn check_child_error(&mut self) -> Option<io::Result<Vec<u8>>> {
        if let Some(child) = &mut self.child {
            let status = child.wait().unwrap();
            if !status.success() {
                return Some(Err(io::Error::new(io::ErrorKind::Other, "Error executing git")));
            }
        }
        None
    }
}

impl Iterator for GitOutputIterator {
    type Item = io::Result<Vec<u8>>;

    fn next(&mut self) -> Option<Self::Item> {
        let result = match &mut self.field_iter {
            None => return None,
            Some(Err(_)) => return Some(Err(self.field_iter.take().unwrap().err().unwrap())),
            Some(Ok(iter)) => iter.next(),
        };
        match &result {
            Some(Ok(_)) => return result,
            Some(Err(_)) => {
                self.check_child_error();
                return result;
            }
            None => self.check_child_error()
        }
    }
}

trait ByteVecUtils {
    fn rstrip(self, what: u8) -> Self;
    fn bytes_parse<F: FromStr>(&self) -> Option<F>;
}

impl ByteVecUtils for Vec<u8> {
    fn rstrip(mut self, what: u8) -> Self {
        while !self.is_empty() && self[self.len() - 1] == what {
            self.pop();
        }
        self
    }

    fn bytes_parse<F: FromStr>(&self) -> Option<F> {
        std::str::from_utf8(self).ok().and_then(|s| s.parse().ok())
    }
}

impl Description {
    pub fn diff(&self, repo: &Git) -> &Lines {
        self.diff_lines.get_or_init(
            || match repo.diff(&self.id) {
                Ok(lines) => lines,
                Err(err) => err.to_string().into_bytes().into()
            }
        )
    }
}

impl PartialEq for Description {
    fn eq(&self, other: &Self) -> bool {
        self.id == other.id
    }
}

impl Eq for Description {}

impl Ord for Description {
    fn cmp(&self, other: &Self) -> Ordering {
        self.commit_timestamp.cmp(&other.commit_timestamp)
            .then_with(|| self.author_timestamp.cmp(&other.author_timestamp))
            .then_with(|| self.id.cmp(&other.id))
    }
}

impl PartialOrd for Description {
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
        Some(self.cmp(other))
    }
}
