#!/usr/bin/env node

var fs = require('fs');

var Promise = require('bluebird');
var git = require('nodegit');
var program = require('commander');
var _ = require('lodash');

var exec = Promise.promisify(require('child_process').exec);

var config = {
    // Maximum time diff between 2 subsequent commits in minutes which are
    // counted to be in the same coding "session"
    maxCommitDiffInMinutes: 2 * 60,

    // How many minutes should be added for the first commit of coding session
    firstCommitAdditionInMinutes: 2 * 60
};

function main() {
    parseArgs();
    config = mergeDefaultsWithArgs(config);

    commits('.').then(function(commits) {
        var work = {
            total: {
                hours: estimateHours(_.pluck(commits, 'date')),
                commits: commits.length
            }
        };

        console.log(JSON.stringify(work, undefined, 2));
    }).catch(function(e) {
        console.error(e.stack);
    });
}

function parseArgs() {
    function list(val) {
        return val.split(',');
    }

    function int(val) {
        return parseInt(val, 10);
    }

    program
        .version(require('./package.json').version)
        .usage('[options]')
        .option('-b, --branches [branches]', 'list of branches to calculate commits from e.g. master,dev', list)
        .option('-d, --max-commit-diff [max-commit-diff]', 'maximum difference in minutes between commits counted to one session', int)
        .option('-a, --first-commit-add [first-commit-add]', 'how many minutes first commit of session should add to total', int)

    program.on('--help', function() {
        console.log('  Examples:');
        console.log('');
        console.log('   - Estimate hours of project');
        console.log('');
        console.log('       $ githours');
        console.log('');
        console.log('   - Estimate hours of development branch');
        console.log('');
        console.log('       $ githours --branches development');
        console.log('');
        console.log('   - Estimate hours in repository where developers commit more seldom: they might have 4h(240min) pause between commits');
        console.log('');
        console.log('       $ githours --max-commit-diff 240');
        console.log('');
        console.log('   - Estimate hours in repository where developer works 5 hours before first commit in day');
        console.log('');
        console.log('       $ githours --first-commit-add 300');
        console.log('');
        console.log('  For more details, visit https://github.com/kimmobrunfeldt/githours');
        console.log('');
    });

    program.parse(process.argv);
}

function mergeDefaultsWithArgs(config) {
    return {
        branches: program.branches || [],
        maxCommifDiffInMinutes: program.maxCommitDiff || config.maxCommitDiffInMinutes,
        firstCommitAdditionInMinutes: program.firstCommitAdd || config.firstCommitAdditionInMinutes
    };
}

// Estimates spent working hours based on commit dates
function estimateHours(dates) {
    if (dates.length < 2) {
        return 0;
    }

    // Oldest commit first, newest last
    var sortedDates = dates.sort(function(a, b) {
        return a - b;
    });
    var allButLast = _.take(sortedDates, sortedDates.length - 1);

    var hours = _.reduce(allButLast, function(hours, date, index) {
        var nextDate = sortedDates[index + 1];
        var diffInMinutes = (nextDate - date) / 1000 / 60;

        // Check if commits are counted to be in same coding session
        if (diffInMinutes < config.maxCommitDiffInMinutes) {
            return hours + (diffInMinutes / 60);
        }

        // The date difference is too big to be inside single coding session
        // The work of first commit of a session cannot be seen in git history,
        // so we make a blunt estimate of it
        return hours + (config.firstCommitAdditionInMinutes / 60);
    }, 0);

    return Math.round(hours);
}

// Promisify nodegit's API of getting all commits in repository
function commits(gitPath) {
    // Promisifing nodegit did not work.
    return new Promise(function(resolve, reject) {
        git.Repo.open(gitPath, function(err, repo) {
            if (err) {
                reject(err);
                return;
            }

            var branchNames = config.branches;
            if (_.isEmpty(branchNames)) {
                // If no command line parameters set, get all branches
                branchNames = getBranchNames(gitPath);
            }

            branchNames.map(function(branchName) {
                return getBranch(repo, branchName);
            }).map(function(branch) {
                return getBranchCommits(branch);
            }).reduce(function(allCommits, branchCommits) {
                _.each(branchCommits, function(commit) {
                    allCommits.push(commit);
                });

                return allCommits;
            }, []).then(function(commits) {
                var uniqueCommits = _.uniq(commits, function(item, key, a) {
                    return item.sha;
                });

                resolve(uniqueCommits);
            }).catch(reject);
        });
    });
}

function getBranchNames(gitPath) {
    var cmd = "git branch --no-color | awk -F ' +' '! /\\(no branch\\)/ {print $2}'";
    return new Promise(function(resolve, reject) {
        exec(cmd, {cwd: gitPath}, function(err, stdout, stderr) {
            if (err) {
                reject(err);
            }

            resolve(stdout
                    .split('\n')
                    .filter(function(e) { return e; })  // Remove empty
                    .map(function(str) { return str.trim(); })  // Trim whitespace
            );
        });
    });
}

function getBranches(repo, names) {
    var branches = [];
    for (var i = 0; i < names.length; ++i) {
        branches.push(getBranch(repo, names[i]));
    }

    return Promise.all(branches);
}

function getBranch(repo, name) {
    return new Promise(function(resolve, reject) {
        repo.getBranch(name, function(err, branch) {
            if (err) {
                reject(err);
                return;
            }

            resolve(branch);
        });
    });
}

function getBranchCommits(branch) {
    return new Promise(function(resolve, reject) {
        var history = branch.history();
        var commits = [];

        history.on("commit", function(commit) {
            var author = null;
            if (!_.isNull(commit.author())) {
                author = {
                    name: commit.author().name(),
                    email: commit.author().email()
                };
            }

            var commitData = {
                sha: commit.sha(),
                date: commit.date(),
                message: commit.message(),
                author: author
            };

            commits.push(commitData);
        });

        history.on("end", function() {
            resolve(commits);
        });

        history.on("error", function(err) {
            reject(err);
        });

        // Start emitting events.
        history.start();
    });
}

main();

@@ -4515,32 +4515,44 @@
  %7D%0A%0A            
+Promise.map(
 branchNames.map(
@@ -4546,21 +4546,18 @@
 nchNames
-.map(
+, 
 function

Fix branches command line parameter