use bstr::ByteSlice;
use crate::common::git;
use super::format::{Formatted, Style};
use std::{ffi::OsString, rc::Rc};
use std::os::unix::ffi::OsStrExt;

pub trait ToDescription {
    fn to_description(&self, repo: &git::Git) -> Result<Rc<git::Description>, String>;
}

pub trait DescriptionFormat {
    fn fmt_oneline(&self) -> Formatted;
    fn fmt_header(&self) -> DescriptionHeaderIterator<'_>;
}

impl ToDescription for Rc<git::Description> {
    fn to_description(&self, _repo: &git::Git) -> Result<Rc<git::Description>, String> {
        Ok(self.clone())
    }
}

impl ToDescription for &[u8] {
    fn to_description(&self, repo: &git::Git) -> Result<Rc<git::Description>, String> {
        let description = repo.description(self)
            .map_err(|err| format!(
                "Cannot find commit {}: {}",
                self.as_bstr(),
                err.to_string()
            ))?;
        Ok(Rc::new(description))
    }
}

impl ToDescription for &str {
    fn to_description(&self, repo: &git::Git) -> Result<Rc<git::Description>, String> {
        self.as_bytes().to_description(repo)
    }
}

impl<const N: usize> ToDescription for &[u8; N] {
    fn to_description(&self, repo: &git::Git) -> Result<Rc<git::Description>, String> {
        self.as_slice().to_description(repo)
    }
}

impl ToDescription for &OsString {
    fn to_description(&self, repo: &git::Git) -> Result<Rc<git::Description>, String> {
        self.as_bytes().to_description(repo)
    }
}

impl DescriptionFormat for git::Description {
    fn fmt_oneline(&self) -> Formatted {
        let mut fmt = Formatted::default();
        fmt.push(&self.id[..12]);
        fmt.push(" ");
        fmt.push_style(&self.subject, Style::Italic);
        fmt
    }

    fn fmt_header(&self) -> DescriptionHeaderIterator<'_> {
        DescriptionHeaderIterator {
            description: self,
            line: DescriptionHeaderLine::default(),
        }
    }
}

#[derive(Default)]
enum DescriptionHeaderLine {
    #[default] Id,
    Author,
    AuthorDate,
    Empty1,
    Subject,
    Empty2,
    Body(usize),
}

pub struct DescriptionHeaderIterator<'a> {
    description: &'a git::Description,
    line: DescriptionHeaderLine,
}

impl<'a> Iterator for DescriptionHeaderIterator<'a> {
    type Item = Formatted;

    fn next(&mut self) -> Option<Self::Item> {
        let mut fmt = Formatted::default();
        let next_line = match self.line {
            DescriptionHeaderLine::Id => {
                fmt.push("commit ");
                fmt.push_style(&self.description.id, Style::Color(0x4488ff));
                DescriptionHeaderLine::Author
            }
            DescriptionHeaderLine::Author => {
                fmt.push("Author: ");
                fmt.push_style(&self.description.author, Style::Color(0xff44aa));
                DescriptionHeaderLine::AuthorDate
            }
            DescriptionHeaderLine::AuthorDate => {
                fmt.push("Date: ");
                fmt.push_style(&self.description.author_date, Style::Color(0xaa8844));
                DescriptionHeaderLine::Empty1
            }
            DescriptionHeaderLine::Empty1 => DescriptionHeaderLine::Subject,
            DescriptionHeaderLine::Subject => {
                fmt.push_style(&self.description.subject, Style::Color(0x009900));
                DescriptionHeaderLine::Empty2
            }
            DescriptionHeaderLine::Empty2 => DescriptionHeaderLine::Body(0),
            DescriptionHeaderLine::Body(idx) => {
                let body_len = self.description.body.len();
                if idx >= body_len {
                    return None;
                }
                let pos = self.description.body[idx ..].iter().position(|&b| b == b'\n');
                let (range, next) = match pos {
                    Some(pos) => (idx .. idx + pos, idx + pos + 1),
                    None => (idx .. body_len, body_len),
                };
                if range.is_empty() && next >= body_len {
                    // ignore the last empty line
                    return None;
                }
                fmt.push_style(&self.description.body[range], Style::Monospace);
                DescriptionHeaderLine::Body(next)
            }
        };
        self.line = next_line;
        Some(fmt)
    }
}
