#!/usr/bin/env run-cargo-script
// cargo-deps: regex="1.3.9"
use std::{ env, fs, process };
use std::path::PathBuf;
use std::os::unix::fs::PermissionsExt;

const DEFAULT_COMMIT_MSG_REGEXP: &str = "^(feat|fix|docs|style|refactor|test|chore)(\\([\\w\\-_.]+\\))?:\\s*.*";

/// Git commit message format check util
///
/// Commit format MUST be <type>(<scope>): subject
///
/// Usage:
/// `commit-msg.crs usage`
///
/// Install:
/// `commit-msg.crs install`
fn main() {
    if is_verbose() { print_info(&format!("Arguments:\n- {}", env::args().collect::<Vec<_>>().join("\n- "))); }
    let arg1 = env::args().nth(1).unwrap_or_else(|| {
        exit_with_error_message("Commit message is EMPTY!");
    });

    if arg1 == "usage" || arg1 == "USAGE" {
        print_usage(); return;
    }

    let is_install = arg1 == "install" || arg1 == "INSTALL";
    let is_force_install = arg1 == "forceinstall" || arg1 == "FORCEINSTALL"
                        || arg1 == "force-install" || arg1 == "FORCE-INSTALL"
                        || arg1 == "installforce" || arg1 == "INSTALLFORCE"
                        || arg1 == "install-force" || arg1 == "INSTALL-FORCE";
    if is_install || is_force_install {
        install_commit_msg(is_force_install); return;
    }

    let commit_message = fs::read_to_string(arg1).unwrap_or_else(|e| {
        exit_with_error_message(&format!("Read commit message failed: {}", e));
    });

    let re_str = get_commit_msg_regexp();
    let re = match regex::Regex::new(re_str.trim()) {
        Ok(re) => re, Err(e) => exit_with_error_message(&format!("Parse regexp: {}, failed: {}", re_str, e)),
    };
    let is_commit_message_matches = re.is_match(&commit_message);

    print_info(&format!("Commit message: {}{}{}", UNDER, commit_message.trim(), END));
    if is_commit_message_matches {
        print_ok("Commit message rule matches");
    } else {
        print_usage();
        exit_with_error_message("Commit message rule is NOT matched");
    }
}

fn print_usage() {
    if let Some(usage) = get_commit_msg_usage() {
        print_info(&usage); return;
    }
    print_info(&format!(r#"Please follow the commit spec:
Format: {b}<type>{e}(<scope>){b}: <subject>{e}
               <scope> is optional

{b}feat{e}: add hat wobble
^--^  ^------------^
|     |
|     +-> Summary in present tense.
|
+-------> Type: chore, docs, feat, fix, refactor, style, or test.

{b}feat{e}     : new feature for the user, not a new feature for build script
{b}fix{e}      : bug fix for the user, not a fix to a build script
{b}docs{e}     : changes to the documentation
{b}style{e}    : formatting, missing semi colons, etc; no production code change
{b}refactor{e} : refactoring production code, eg. renaming a variable
{b}test{e}     : adding missing tests, refactoring tests; no production code change
{b}chore{e}    : updating grunt tasks etc; no production code change

Reference: {u}https://gist.github.com/joshbuchea/6f47e86d2510bce28f8e7f42ae84c716{e}
or mirror: {u}https://hatter.ink/wiki/view_raw.action?__access_token=PUBLIC&id=42{e}
"#, b = BOLD, e = END, u = UNDER));
}

fn get_commit_msg_file(file: &str) -> Option<String> {
    let settings_regexp = search_git_root_path_e().join("settings").join(file);
    if is_verbose() { print_info(&format!("Check file: {:?}, exists: {}", settings_regexp, settings_regexp.exists())); }
    if settings_regexp.exists() {
        print_info(&format!("Found file: {:?}", settings_regexp));
        return Some(fs::read_to_string(settings_regexp).unwrap());
    }

    let path_regexp = search_git_root_path_e().join(".git").join("hooks").join(file);
    if is_verbose() { print_info(&format!("Check file: {:?}, exists: {}", path_regexp, path_regexp.exists())); }
    if path_regexp.exists() {
        print_info(&format!("Found file: {:?}", path_regexp));
        return Some(fs::read_to_string(path_regexp).unwrap());
    }

    let home_path_regexp = get_home_e().join(&(".".to_owned() + file));
    if is_verbose() { print_info(&format!("Check file: {:?}, exists: {}", home_path_regexp, home_path_regexp.exists())); }
    if home_path_regexp.exists() {
        print_info(&format!("Found file: {:?}", home_path_regexp));
        return Some(fs::read_to_string(home_path_regexp).unwrap());
    }

    None
}

fn get_commit_msg_regexp() -> String {
    get_commit_msg_file("commit-msg-regexp").unwrap_or_else(|| DEFAULT_COMMIT_MSG_REGEXP.to_owned())
}

fn get_commit_msg_usage() -> Option<String> {
    get_commit_msg_file("commit-msg-regexp-usage")
}

fn copy_file_and_apply_executable_permission(from: &PathBuf, to: &PathBuf) {
    print_info(&format!("Copy file: {:?} to: {:?}", from, to));
    let from_content = fs::read(&from).unwrap_or_else(|e|
        exit_with_error_message(&format!("Read file: {:?}, failed: {}", from, e))
    );
    fs::write(to, from_content).unwrap_or_else(|e|
        exit_with_error_message(&format!("Write file: {:?}, failed: {}", to, e))
    );
    fs::set_permissions(to, PermissionsExt::from_mode(0o755)).unwrap_or_else(|e|
        exit_with_error_message(&format!("Apply executable permission on file: {:?}, failed: {}", to, e))
    );
}

fn install_commit_msg(force: bool) {
    let commit_msg_crs = get_home_e().join("bin").join("commit-msg.crs");
    let commig_msg_exec_file = if commit_msg_crs.exists() {
        commit_msg_crs
    } else {
        print_warn(&format!("File {:?} NOT exists!", commit_msg_crs));
        PathBuf::from(env::args().next().unwrap())
    };

    let git_hooks = search_git_root_path_e().join(".git").join("hooks");
    if !git_hooks.exists() {
        exit_with_error_message(&format!("Path {:?} NOT exists!", git_hooks));
    }
    let git_hooks_commit_msg = git_hooks.join("commit-msg");
    if git_hooks_commit_msg.exists() && !force {
        exit_with_error_message(&format!("File {:?} exists! or try {}forceinstall{}.", git_hooks_commit_msg, BOLD, END));
    }

    copy_file_and_apply_executable_permission(&commig_msg_exec_file, &git_hooks_commit_msg);

    print_ok("Install commit-msg to repo successed!");
}


fn search_git_root_path() -> Option<PathBuf> {
    let mut p = PathBuf::from(".").canonicalize().unwrap_or_else(|e|
        exit_with_error_message(&format!("Get current dir failed: {}", e))
    );
    loop {
        if is_verbose() { print_info(&format!("Check git path: {:?}", p)); }
        if p.join(".git").is_dir() {
            if is_verbose() { print_ok(&format!("Found git path: {:?}", p)); }
            return Some(p);
        }
        if !p.pop() { return None; }
    }
}

fn search_git_root_path_e() -> PathBuf {
    search_git_root_path().unwrap_or_else(|| exit_with_error_message("Git root path not found!"))
}

const RED:    &str = "\x1B[91m";
const GREEN:  &str = "\x1B[92m";
const YELLOW: &str = "\x1B[93m";
const BOLD:   &str = "\x1B[1m";
const UNDER:  &str = "\x1B[4m";
const END:    &str = "\x1B[0m";

fn get_home_e() -> PathBuf {
    PathBuf::from(std::env::var("HOME").unwrap_or_else(|e| exit_with_error_message(&format!("Get env HOME failed: {}!", e))))
}

fn is_verbose() -> bool { if let Ok(v) = std::env::var("VERBOSE") { v == "1" } else { false } }

fn exit_with_error() -> ! { process::exit(1) }
fn exit_with_error_message(msg: &str) -> ! { print_error(msg); exit_with_error() }

fn print_info (msg: &str) { println!("{b}[INFO ]{e} {m}",                      b = BOLD, e = END, m = msg); }
fn print_ok   (msg: &str) { println!("{g}{b}[OK   ]{e} {g}{m}{e}", g = GREEN,  b = BOLD, e = END, m = msg); }
fn print_warn (msg: &str) { println!("{y}{b}[WARN ]{e} {y}{m}{e}", y = YELLOW, b = BOLD, e = END, m = msg); }
fn print_error(msg: &str) { println!("{r}{b}[ERROR]{e} {r}{m}{e}", r = RED,    b = BOLD, e = END, m = msg); }
