use std::sync::atomic::{AtomicUsize, Ordering};

use gix::bstr::{BStr, ByteSlice};

use crate::hours::core::HOURS_PER_WORKDAY;

#[derive(Debug)]
pub struct WorkByPerson {
    pub name: Vec<&'static BStr>,
    pub email: Vec<&'static BStr>,
    pub hours: f32,
    pub num_commits: u32,
    pub files: FileStats,
    pub lines: LineStats,
}

impl WorkByPerson {
    pub fn merge(&mut self, other: &WorkByEmail) {
        if !self.name.contains(&other.name) {
            self.name.push(other.name);
        }
        if !self.email.contains(&other.email) {
            self.email.push(other.email);
        }
        self.num_commits += other.num_commits;
        self.hours += other.hours;
        self.files.add(&other.files);
        self.lines.add(&other.lines);
    }
}

impl<'a> From<&'a WorkByEmail> for WorkByPerson {
    fn from(w: &'a WorkByEmail) -> Self {
        WorkByPerson {
            name: vec![w.name],
            email: vec![w.email],
            hours: w.hours,
            num_commits: w.num_commits,
            files: w.files,
            lines: w.lines,
        }
    }
}

/// Combine all iterator elements into one String, separated by `sep`.
///
/// Use the `Display` implementation of each element.
///
/// extracted from
/// https://github.com/rust-itertools/itertools/blob/762643f1be2217140a972745cf4d6ed69435f722/src/lib.rs#L2295-L2324
fn join<I>(mut iter: I, sep: &str) -> String
where
    I: Iterator,
    <I as Iterator>::Item: std::fmt::Display,
{
    use ::std::fmt::Write;

    match iter.next() {
        None => String::new(),
        Some(first_elt) => {
            // estimate lower bound of capacity needed
            let (lower, _) = iter.size_hint();
            let mut result = String::with_capacity(sep.len() * lower);
            write!(&mut result, "{first_elt}").expect("enough memory");
            iter.for_each(|elt| {
                result.push_str(sep);
                write!(&mut result, "{elt}").expect("enough memory");
            });
            result
        }
    }
}

impl WorkByPerson {
    pub fn write_to(
        &self,
        total_hours: f32,
        total_files: Option<FileStats>,
        total_lines: Option<LineStats>,
        mut out: impl std::io::Write,
    ) -> std::io::Result<()> {
        writeln!(
            out,
            "{names} <{mails}>",
            names = join(self.name.iter(), ", "),
            mails = join(self.email.iter(), ", ")
        )?;
        writeln!(out, "{} commits found", self.num_commits)?;
        writeln!(
            out,
            "total time spent: {:.02}h ({:.02} 8h days, {:.02}%)",
            self.hours,
            self.hours / HOURS_PER_WORKDAY,
            (self.hours / total_hours) * 100.0
        )?;
        if let Some(total) = total_files {
            writeln!(
                out,
                "total files added/removed/modified: {}/{}/{} ({:.02}%)",
                self.files.added,
                self.files.removed,
                self.files.modified,
                (self.files.sum() / total.sum()) * 100.0
            )?;
        }
        if let Some(total) = total_lines {
            writeln!(
                out,
                "total lines added/removed: {}/{} ({:.02}%)",
                self.lines.added,
                self.lines.removed,
                (self.lines.sum() / total.sum()) * 100.0
            )?;
        }
        Ok(())
    }
}

#[derive(Debug)]
pub struct WorkByEmail {
    pub name: &'static BStr,
    pub email: &'static BStr,
    pub hours: f32,
    pub num_commits: u32,
    pub files: FileStats,
    pub lines: LineStats,
}

/// File statistics for a particular commit.
#[derive(Debug, Default, Copy, Clone)]
pub struct FileStats {
    /// amount of added files
    pub added: usize,
    /// amount of removed files
    pub removed: usize,
    /// amount of modified files
    pub modified: usize,
}

/// Line statistics for a particular commit.
#[derive(Debug, Default, Copy, Clone)]
pub struct LineStats {
    /// amount of added lines
    pub added: usize,
    /// amount of removed lines
    pub removed: usize,
}

impl FileStats {
    pub fn add(&mut self, other: &FileStats) -> &mut Self {
        self.added += other.added;
        self.removed += other.removed;
        self.modified += other.modified;
        self
    }

    pub fn sum(&self) -> f32 {
        (self.added + self.removed + self.modified) as f32
    }
}

impl LineStats {
    pub fn add(&mut self, other: &LineStats) -> &mut Self {
        self.added += other.added;
        self.removed += other.removed;
        self
    }

    pub fn sum(&self) -> f32 {
        (self.added + self.removed) as f32
    }
}

/// An index able to address any commit
pub type CommitIdx = u32;

pub fn add_lines(line_stats: bool, lines_counter: &AtomicUsize, lines: &mut LineStats, id: gix::Id<'_>) {
    if let Some(Ok(blob)) = line_stats.then(|| id.object()) {
        let nl = blob.data.lines_with_terminator().count();
        lines.added += nl;
        lines_counter.fetch_add(nl, Ordering::Relaxed);
    }
}

pub fn remove_lines(line_stats: bool, lines_counter: &AtomicUsize, lines: &mut LineStats, id: gix::Id<'_>) {
    if let Some(Ok(blob)) = line_stats.then(|| id.object()) {
        let nl = blob.data.lines_with_terminator().count();
        lines.removed += nl;
        lines_counter.fetch_add(nl, Ordering::Relaxed);
    }
}
