ttdl 6.1.0

TTDL - Terminal ToDo List manager
use std::cmp::Ordering;
use std::io::{self, Write};

use crate::fmt;
use caseless::default_caseless_match_str;
use termcolor::StandardStream;
use todo_lib::*;

const TOTAL: &str = "";
const PRJ_WIDTH: usize = 15;
const CTX_WIDTH: usize = 10;
const NUM_WIDTH: usize = 10;

fn is_task_overdue(task: &todotxt::Task, today: chrono::NaiveDate) -> bool {
    match task.due_date {
        None => false,
        Some(d) => {
            if let Some(f) = task.finish_date {
                d > f
            } else {
                !task.finished && d < today
            }
        }
    }
}

pub fn show_stats(stdout: &mut StandardStream, tasks: &todo::TaskSlice, conf: &fmt::Conf) -> io::Result<()> {
    show_short_stats(stdout, tasks)?;
    if conf.fmt != fmt::Format::Short {
        writeln!(stdout)?;
        show_full_stats(stdout, tasks)?;
    }
    Ok(())
}

fn show_short_stats(stdout: &mut StandardStream, tasks: &todo::TaskSlice) -> io::Result<()> {
    let mut overdue: usize = 0;
    let mut recurrent: usize = 0;
    let mut done: usize = 0;
    let today = chrono::Utc::now().naive_utc().date();

    for t in tasks.iter() {
        if is_task_overdue(t, today) {
            overdue += 1;
        }
        if t.finished {
            done += 1;
            continue;
        }
        if t.recurrence.is_some() {
            recurrent += 1;
        }
    }

    let width = 19;
    let length = tasks.len();
    writeln!(stdout, "{:wid$}{:4}", "Total todos:", length, wid = width)?;
    if done > 0 {
        writeln!(stdout, "{:wid$}{:>4} ({}%)", "Done:", done, done * 100 / length, wid = width)?;
    }
    if overdue > 0 {
        writeln!(stdout, "{:wid$}{:4} ({}%)", "Overdue:", overdue, overdue * 100 / length, wid = width)?;
    }
    if recurrent > 0 {
        writeln!(stdout, "{:wid$}{:4}", "Recurrent:", recurrent, wid = width)?;
    }
    Ok(())
}

struct Stat {
    prj: String,
    ctx: String,
    total: usize,
    done: usize,
    overdue: usize,
    spent: chrono::Duration,
}
struct Stats {
    stats: Vec<Stat>,
}
impl Stats {
    fn update_stat(&mut self, prj: &str, ctx: &str, done: bool, overdue: bool, spent: chrono::Duration) {
        let mut found = false;
        for t in self.stats.iter_mut() {
            if default_caseless_match_str(&t.prj, prj) && default_caseless_match_str(&t.ctx, ctx) {
                t.total += 1;
                if done {
                    t.done += 1;
                }
                if overdue {
                    t.overdue += 1;
                }
                t.spent += spent;
                found = true;
            }
        }

        if !found {
            let s = Stat {
                prj: prj.to_lowercase(),
                ctx: ctx.to_lowercase(),
                total: 1,
                done: usize::from(done),
                overdue: usize::from(overdue),
                spent,
            };
            self.stats.push(s);
        }
    }
    fn sort(&mut self) {
        self.stats.sort_by(|a, b| {
            if a.prj == TOTAL {
                return Ordering::Less;
            }
            if b.prj == TOTAL {
                return Ordering::Greater;
            }

            let c = a.prj.to_lowercase().cmp(&b.prj.to_lowercase());
            if c != Ordering::Equal {
                return c;
            }
            if a.ctx == TOTAL {
                return Ordering::Less;
            }
            if b.ctx == TOTAL {
                return Ordering::Greater;
            }
            a.ctx.to_lowercase().cmp(&b.ctx.to_lowercase())
        });
    }
}

fn show_full_stats(stdout: &mut StandardStream, tasks: &todo::TaskSlice) -> io::Result<()> {
    let mut st = Stats { stats: Vec::new() };
    let today = chrono::Utc::now().naive_utc().date();

    for t in tasks.iter() {
        let done = t.finished;
        let overdue = is_task_overdue(t, today);
        let spent = timer::spent_time(t);
        st.update_stat(TOTAL, TOTAL, done, overdue, spent);
        for p in t.projects.iter() {
            st.update_stat(p, TOTAL, done, overdue, spent);
            for c in t.contexts.iter() {
                st.update_stat(p, c, done, overdue, spent);
            }
        }
    }

    st.sort();
    let mut last_prj = "".to_string();
    let mut proj_total: usize = 0;

    let length = tasks.len();
    let header = format!(
        "{:pw$} {:cw$} {:nw$} {:nw$} {:nw$} {}",
        "Project",
        "Context",
        "Total",
        "Done",
        "Overdue",
        "Spent",
        pw = PRJ_WIDTH,
        cw = CTX_WIDTH,
        nw = NUM_WIDTH
    );
    writeln!(stdout, "{header}")?;
    let sep = "-".repeat(header.len());

    for s in st.stats.iter() {
        let prj = if s.prj == last_prj {
            String::new()
        } else {
            writeln!(stdout, "{sep}")?;
            last_prj.clone_from(&s.prj);
            proj_total = s.total;
            if s.prj.len() > PRJ_WIDTH { format!("{:.w$}", s.prj, w = PRJ_WIDTH) } else { s.prj.clone() }
        };
        write!(stdout, "{prj:PRJ_WIDTH$} ")?;

        let ctx = if s.ctx.len() > CTX_WIDTH { format!("{:.w$}", s.ctx, w = CTX_WIDTH) } else { s.ctx.clone() };
        write!(stdout, "{ctx:CTX_WIDTH$} ")?;

        let div_by = if s.ctx.is_empty() { length } else { proj_total };
        let sn = format!("{}({:>3}%)", s.total, s.total * 100 / div_by);
        write!(stdout, "{:>w$} ", &sn, w = NUM_WIDTH)?;

        let sn = if s.done == 0 { String::new() } else { format!("{}({:>3}%)", s.done, s.done * 100 / s.total) };
        write!(stdout, "{:>w$} ", &sn, w = NUM_WIDTH)?;

        let sn =
            if s.overdue == 0 { String::new() } else { format!("{}({:>3}%)", s.overdue, s.overdue * 100 / s.total) };
        write!(stdout, "{:>w$} ", &sn, w = NUM_WIDTH)?;

        if s.spent.num_seconds() == 0 {
            writeln!(stdout)?;
        } else {
            writeln!(stdout, "{}", &fmt::duration_str(s.spent))?;
        }
    }
    Ok(())
}