//! Formatting of commits.

use chrono::{FixedOffset, Local, TimeZone};
use git2::{Commit, Time};
use lazy_static::lazy_static;
use std::fmt::Write;
use std::str::FromStr;
use textwrap::Options;
use yansi::Paint;

/// Commit formatting options.
#[derive(Ord, PartialOrd, Eq, PartialEq)]
pub enum CommitFormat {
    OneLine,
    Short,
    Medium,
    Full,
    Format(String),
}

impl FromStr for CommitFormat {
    type Err = String;

    fn from_str(str: &str) -> Result<Self, Self::Err> {
        match str {
            "oneline" | "o" => Ok(CommitFormat::OneLine),
            "short" | "s" => Ok(CommitFormat::Short),
            "medium" | "m" => Ok(CommitFormat::Medium),
            "full" | "f" => Ok(CommitFormat::Full),
            str => Ok(CommitFormat::Format(str.to_string())),
        }
    }
}

const NEW_LINE: usize = 0;
const HASH: usize = 1;
const HASH_ABBREV: usize = 2;
const PARENT_HASHES: usize = 3;
const PARENT_HASHES_ABBREV: usize = 4;
const REFS: usize = 5;
const SUBJECT: usize = 6;
const AUTHOR: usize = 7;
const AUTHOR_EMAIL: usize = 8;
const AUTHOR_DATE: usize = 9;
const AUTHOR_DATE_SHORT: usize = 10;
const AUTHOR_DATE_RELATIVE: usize = 11;
const COMMITTER: usize = 12;
const COMMITTER_EMAIL: usize = 13;
const COMMITTER_DATE: usize = 14;
const COMMITTER_DATE_SHORT: usize = 15;
const COMMITTER_DATE_RELATIVE: usize = 16;
const BODY: usize = 17;
const BODY_RAW: usize = 18;

const MODE_SPACE: usize = 1;
const MODE_PLUS: usize = 2;
const MODE_MINUS: usize = 3;

lazy_static! {
    pub static ref PLACEHOLDERS: Vec<[String; 4]> = {
        let base = vec![
            "n", "H", "h", "P", "p", "d", "s", "an", "ae", "ad", "as", "ar", "cn", "ce", "cd",
            "cs", "cr", "b", "B",
        ];
        base.iter()
            .map(|b| {
                [
                    format!("%{}", b),
                    format!("% {}", b),
                    format!("%+{}", b),
                    format!("%-{}", b),
                ]
            })
            .collect()
    };
}

/// Format a commit for `CommitFormat::Format(String)`.
pub fn format_commit(
    format: &str,
    commit: &Commit,
    branches: String,
    wrapping: &Option<Options>,
    hash_color: Option<u8>,
) -> Result<Vec<String>, String> {
    let mut replacements = vec![];

    for (idx, arr) in PLACEHOLDERS.iter().enumerate() {
        let mut curr = 0;
        loop {
            let mut found = false;
            for (mode, str) in arr.iter().enumerate() {
                if let Some(start) = &format[curr..format.len()].find(str) {
                    replacements.push((curr + start, str.len(), idx, mode));
                    curr += start + str.len();
                    found = true;
                    break;
                }
            }
            if !found {
                break;
            }
        }
    }

    replacements.sort_by_key(|p| p.0);

    let mut lines = vec![];
    let mut out = String::new();
    if replacements.is_empty() {
        write!(out, "{}", format).unwrap();
        add_line(&mut lines, &mut out, wrapping);
    } else {
        let mut curr = 0;
        for (start, len, idx, mode) in replacements {
            if idx == NEW_LINE {
                write!(out, "{}", &format[curr..start]).unwrap();
                add_line(&mut lines, &mut out, wrapping);
            } else {
                write!(out, "{}", &format[curr..start]).unwrap();
                let id = commit.id();
                match idx {
                    HASH => {
                        match mode {
                            MODE_SPACE => write!(out, " ").unwrap(),
                            MODE_PLUS => add_line(&mut lines, &mut out, wrapping),
                            _ => {}
                        }
                        if let Some(color) = hash_color {
                            write!(out, "{}", id.to_string().fixed(color))
                        } else {
                            write!(out, "{}", id)
                        }
                    }
                    HASH_ABBREV => {
                        match mode {
                            MODE_SPACE => write!(out, " ").unwrap(),
                            MODE_PLUS => add_line(&mut lines, &mut out, wrapping),
                            _ => {}
                        }
                        if let Some(color) = hash_color {
                            write!(out, "{}", id.to_string()[..7].fixed(color))
                        } else {
                            write!(out, "{}", &id.to_string()[..7])
                        }
                    }
                    PARENT_HASHES => {
                        match mode {
                            MODE_SPACE => write!(out, " ").unwrap(),
                            MODE_PLUS => add_line(&mut lines, &mut out, wrapping),
                            _ => {}
                        }
                        for i in 0..commit.parent_count() {
                            write!(out, "{}", commit.parent_id(i).unwrap()).unwrap();
                            if i < commit.parent_count() - 1 {
                                write!(out, " ").unwrap();
                            }
                        }
                        Ok(())
                    }
                    PARENT_HASHES_ABBREV => {
                        match mode {
                            MODE_SPACE => write!(out, " ").unwrap(),
                            MODE_PLUS => add_line(&mut lines, &mut out, wrapping),
                            _ => {}
                        }
                        for i in 0..commit.parent_count() {
                            write!(
                                out,
                                "{}",
                                &commit
                                    .parent_id(i)
                                    .map_err(|err| err.to_string())?
                                    .to_string()[..7]
                            )
                            .unwrap();
                            if i < commit.parent_count() - 1 {
                                write!(out, " ").unwrap();
                            }
                        }
                        Ok(())
                    }
                    REFS => {
                        match mode {
                            MODE_SPACE => {
                                if !branches.is_empty() {
                                    write!(out, " ").unwrap()
                                }
                            }
                            MODE_PLUS => {
                                if !branches.is_empty() {
                                    add_line(&mut lines, &mut out, wrapping)
                                }
                            }
                            MODE_MINUS => {
                                if branches.is_empty() {
                                    out = remove_empty_lines(&mut lines, out)
                                }
                            }
                            _ => {}
                        }
                        write!(out, "{}", branches)
                    }
                    SUBJECT => {
                        let summary = commit.summary().unwrap_or("");
                        match mode {
                            MODE_SPACE => {
                                if !summary.is_empty() {
                                    write!(out, " ").unwrap()
                                }
                            }
                            MODE_PLUS => {
                                if !summary.is_empty() {
                                    add_line(&mut lines, &mut out, wrapping)
                                }
                            }
                            MODE_MINUS => {
                                if summary.is_empty() {
                                    out = remove_empty_lines(&mut lines, out)
                                }
                            }
                            _ => {}
                        }
                        write!(out, "{}", summary)
                    }
                    AUTHOR => {
                        match mode {
                            MODE_SPACE => write!(out, " ").unwrap(),
                            MODE_PLUS => add_line(&mut lines, &mut out, wrapping),
                            _ => {}
                        }
                        write!(out, "{}", &commit.author().name().unwrap_or(""))
                    }
                    AUTHOR_EMAIL => {
                        match mode {
                            MODE_SPACE => write!(out, " ").unwrap(),
                            MODE_PLUS => add_line(&mut lines, &mut out, wrapping),
                            _ => {}
                        }
                        write!(out, "{}", &commit.author().email().unwrap_or(""))
                    }
                    AUTHOR_DATE => {
                        match mode {
                            MODE_SPACE => write!(out, " ").unwrap(),
                            MODE_PLUS => add_line(&mut lines, &mut out, wrapping),
                            _ => {}
                        }
                        write!(
                            out,
                            "{}",
                            format_date(commit.author().when(), "%a %b %e %H:%M:%S %Y %z")
                        )
                    }
                    AUTHOR_DATE_SHORT => {
                        match mode {
                            MODE_SPACE => write!(out, " ").unwrap(),
                            MODE_PLUS => add_line(&mut lines, &mut out, wrapping),
                            _ => {}
                        }
                        write!(out, "{}", format_date(commit.author().when(), "%F"))
                    }
                    AUTHOR_DATE_RELATIVE => {
                        match mode {
                            MODE_SPACE => write!(out, " ").unwrap(),
                            MODE_PLUS => add_line(&mut lines, &mut out, wrapping),
                            _ => {}
                        }
                        write!(out, "{}", format_relative_time(commit.author().when()))
                    }
                    COMMITTER => {
                        match mode {
                            MODE_SPACE => write!(out, " ").unwrap(),
                            MODE_PLUS => add_line(&mut lines, &mut out, wrapping),
                            _ => {}
                        }
                        write!(out, "{}", &commit.committer().name().unwrap_or(""))
                    }
                    COMMITTER_EMAIL => {
                        match mode {
                            MODE_SPACE => write!(out, " ").unwrap(),
                            MODE_PLUS => add_line(&mut lines, &mut out, wrapping),
                            _ => {}
                        }
                        write!(out, "{}", &commit.committer().email().unwrap_or(""))
                    }
                    COMMITTER_DATE => {
                        match mode {
                            MODE_SPACE => write!(out, " ").unwrap(),
                            MODE_PLUS => add_line(&mut lines, &mut out, wrapping),
                            _ => {}
                        }
                        write!(
                            out,
                            "{}",
                            format_date(commit.committer().when(), "%a %b %e %H:%M:%S %Y %z")
                        )
                    }
                    COMMITTER_DATE_SHORT => {
                        match mode {
                            MODE_SPACE => write!(out, " ").unwrap(),
                            MODE_PLUS => add_line(&mut lines, &mut out, wrapping),
                            _ => {}
                        }
                        write!(out, "{}", format_date(commit.committer().when(), "%F"))
                    }
                    COMMITTER_DATE_RELATIVE => {
                        match mode {
                            MODE_SPACE => write!(out, " ").unwrap(),
                            MODE_PLUS => add_line(&mut lines, &mut out, wrapping),
                            _ => {}
                        }
                        write!(out, "{}", format_relative_time(commit.committer().when()))
                    }
                    BODY => {
                        let message = commit
                            .message()
                            .unwrap_or("")
                            .lines()
                            .collect::<Vec<&str>>();

                        let num_parts = message.len();
                        match mode {
                            MODE_SPACE => {
                                if num_parts > 2 {
                                    write!(out, " ").unwrap()
                                }
                            }
                            MODE_PLUS => {
                                if num_parts > 2 {
                                    add_line(&mut lines, &mut out, wrapping)
                                }
                            }
                            MODE_MINUS => {
                                if num_parts <= 2 {
                                    out = remove_empty_lines(&mut lines, out)
                                }
                            }
                            _ => {}
                        }
                        for (cnt, line) in message.iter().enumerate() {
                            if cnt > 1 && (cnt < num_parts - 1 || !line.is_empty()) {
                                write!(out, "{}", line).unwrap();
                                add_line(&mut lines, &mut out, wrapping);
                            }
                        }
                        Ok(())
                    }
                    BODY_RAW => {
                        let message = commit
                            .message()
                            .unwrap_or("")
                            .lines()
                            .collect::<Vec<&str>>();

                        let num_parts = message.len();

                        match mode {
                            MODE_SPACE => {
                                if !message.is_empty() {
                                    write!(out, " ").unwrap()
                                }
                            }
                            MODE_PLUS => {
                                if !message.is_empty() {
                                    add_line(&mut lines, &mut out, wrapping)
                                }
                            }
                            MODE_MINUS => {
                                if message.is_empty() {
                                    out = remove_empty_lines(&mut lines, out)
                                }
                            }
                            _ => {}
                        }
                        for (cnt, line) in message.iter().enumerate() {
                            if cnt < num_parts - 1 || !line.is_empty() {
                                write!(out, "{}", line).unwrap();
                                add_line(&mut lines, &mut out, wrapping);
                            }
                        }
                        Ok(())
                    }
                    x => return Err(format!("No commit field at index {}", x)),
                }
                .unwrap();
            }
            curr = start + len;
        }
        write!(out, "{}", &format[curr..(format.len())]).unwrap();
        if !out.is_empty() {
            add_line(&mut lines, &mut out, wrapping);
        }
    }
    Ok(lines)
}

/// Format a commit for `CommitFormat::OneLine`.
pub fn format_oneline(
    commit: &Commit,
    branches: String,
    wrapping: &Option<Options>,
    hash_color: Option<u8>,
) -> Vec<String> {
    let mut out = String::new();
    let id = commit.id();
    if let Some(color) = hash_color {
        write!(out, "{}", id.to_string()[..7].fixed(color))
    } else {
        write!(out, "{}", &id.to_string()[..7])
    }
    .unwrap();

    write!(out, "{} {}", branches, commit.summary().unwrap_or("")).unwrap();

    if let Some(wrap) = wrapping {
        textwrap::fill(&out, wrap)
            .lines()
            .map(|str| str.to_string())
            .collect()
    } else {
        vec![out]
    }
}

/// Format a commit for `CommitFormat::Short`, `CommitFormat::Medium` or `CommitFormat::Full`.
pub fn format(
    commit: &Commit,
    branches: String,
    wrapping: &Option<Options>,
    hash_color: Option<u8>,
    format: &CommitFormat,
) -> Result<Vec<String>, String> {
    match format {
        CommitFormat::OneLine => return Ok(format_oneline(commit, branches, wrapping, hash_color)),
        CommitFormat::Format(format) => {
            return format_commit(format, commit, branches, wrapping, hash_color)
        }
        _ => {}
    }

    let mut out_vec = vec![];
    let mut out = String::new();

    let id = commit.id();
    if let Some(color) = hash_color {
        write!(out, "commit {}", id.to_string().fixed(color))
    } else {
        write!(out, "commit {}", &id)
    }
    .map_err(|err| err.to_string())?;

    write!(out, "{}", branches).map_err(|err| err.to_string())?;
    append_wrapped(&mut out_vec, out, wrapping);

    if commit.parent_count() > 1 {
        out = String::new();
        write!(
            out,
            "Merge: {} {}",
            &commit.parent_id(0).unwrap().to_string()[..7],
            &commit.parent_id(1).unwrap().to_string()[..7]
        )
        .map_err(|err| err.to_string())?;
        append_wrapped(&mut out_vec, out, wrapping);
    }

    out = String::new();
    write!(
        out,
        "Author: {} <{}>",
        commit.author().name().unwrap_or(""),
        commit.author().email().unwrap_or("")
    )
    .map_err(|err| err.to_string())?;
    append_wrapped(&mut out_vec, out, wrapping);

    if format > &CommitFormat::Medium {
        out = String::new();
        write!(
            out,
            "Commit: {} <{}>",
            commit.committer().name().unwrap_or(""),
            commit.committer().email().unwrap_or("")
        )
        .map_err(|err| err.to_string())?;
        append_wrapped(&mut out_vec, out, wrapping);
    }

    if format > &CommitFormat::Short {
        out = String::new();
        write!(
            out,
            "Date:   {}",
            format_date(commit.author().when(), "%a %b %e %H:%M:%S %Y %z")
        )
        .map_err(|err| err.to_string())?;
        append_wrapped(&mut out_vec, out, wrapping);
    }

    if format == &CommitFormat::Short {
        out_vec.push("".to_string());
        append_wrapped(
            &mut out_vec,
            format!("    {}", commit.summary().unwrap_or("")),
            wrapping,
        );
        out_vec.push("".to_string());
    } else {
        out_vec.push("".to_string());
        let mut add_line = true;
        for line in commit.message().unwrap_or("").lines() {
            if line.is_empty() {
                out_vec.push(line.to_string());
            } else {
                append_wrapped(&mut out_vec, format!("    {}", line), wrapping);
            }
            add_line = !line.trim().is_empty();
        }
        if add_line {
            out_vec.push("".to_string());
        }
    }

    Ok(out_vec)
}

pub fn format_date(time: Time, format: &str) -> String {
    let offset = FixedOffset::east_opt(time.offset_minutes() * 60).expect("Invalid offset minutes");
    let date = offset
        .timestamp_opt(time.seconds(), 0)
        .single()
        .expect("Invalid timestamp, maybe a fold or gap in local time");
    date.format(format).to_string()
}

/// Format a time as a relative time string (e.g., "21 hours ago", "4 days ago")
pub fn format_relative_time(time: Time) -> String {
    let offset = FixedOffset::east_opt(time.offset_minutes() * 60).expect("Invalid offset minutes");
    let commit_time = Local::from_offset(&offset)
        .timestamp_opt(time.seconds(), 0)
        .single()
        .expect("Invalid timestamp");
    let now = Local::now();
    let duration = now.signed_duration_since(commit_time);

    let seconds = duration.num_seconds();
    let minutes = duration.num_minutes();
    let hours = duration.num_hours();
    let days = duration.num_hours() / 24;
    let weeks = days / 7;
    let months = days / 30;
    let years = days / 365;

    if seconds < 60 {
        format!("{} seconds ago", seconds)
    } else if minutes < 60 {
        format!("{} minutes ago", minutes)
    } else if hours < 24 {
        format!("{} hours ago", hours)
    } else if days < 7 {
        format!("{} days ago", days)
    } else if weeks < 4 {
        format!("{} weeks ago", weeks)
    } else if months < 12 {
        format!("{} months ago", months)
    } else {
        format!("{} years ago", years)
    }
}

fn append_wrapped(vec: &mut Vec<String>, str: String, wrapping: &Option<Options>) {
    if str.is_empty() {
        vec.push(str);
    } else if let Some(wrap) = wrapping {
        vec.extend(
            textwrap::fill(&str, wrap)
                .lines()
                .map(|str| str.to_string()),
        )
    } else {
        vec.push(str);
    }
}

fn add_line(lines: &mut Vec<String>, line: &mut String, wrapping: &Option<Options>) {
    let mut temp = String::new();
    std::mem::swap(&mut temp, line);
    append_wrapped(lines, temp, wrapping);
}

fn remove_empty_lines(lines: &mut Vec<String>, mut line: String) -> String {
    while !lines.is_empty() && lines.last().unwrap().is_empty() {
        line = lines.remove(lines.len() - 1);
    }
    if !lines.is_empty() {
        line = lines.remove(lines.len() - 1);
    }
    line
}
