#!/usr/bin/env node

// Release automation script inspired by
// https://github.com/geddski/grunt-release

var fs = require('fs');
var path = require('path');

var _ = require('lodash');
var S = require('string');
var shell = require('shelljs');
var Mustache = require('mustache');
var semver = require('semver');
var program = require('commander');
var Promise = require('bluebird');


// Message templates use https://github.com/janl/mustache.js
var config = {
    indentation: 2,
    releaseMessage: 'Release {{ version }}',
    backToDevMessage: 'Bump to dev version',
    bumpType: 'patch',
    files: ['package.json'],
    readmeFile: 'README.md',

    // Banner to insert in beginning of distributables
    bannerFiles: ['dist/progressbar.js', 'dist/progressbar.min.js'],
    banner: '// ProgressBar.js {{ version }}\n// https://kimmobrunfeldt.github.io/progressbar.js\n// License: MIT\n\n',

    // If true, don't execute anything, just tell what would have been done
    dryRun: false,

    // If true, don't push commits/tags or release to npm
    noPush: false,
    consolePrefix: '->',
    devSuffix: '-dev'
}

var projectRoot = path.join(__dirname, '..');
process.chdir(projectRoot);

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

    if (config.dryRun) status('Dry run\n');

    var newVersion = bumpVersion(config.files, config.bumpType);

    var banner = Mustache.render(config.banner, {
        version: newVersion
    });
    insertBanner(config.bannerFiles, banner);

    _gitBranchName()
    .then(function(stdout) {
        if (stdout.trim().toLowerCase() !== 'master') {
            throw new Error('You should be in master branch before running the script!');
        }

        return gitAdd(config.bannerFiles);
    })
    .then(function() {
        return gitAdd([config.readmeFile]);
    })
    .then(function() {
        return gitAdd(config.files);
    })
    .then(function() {
        var message = Mustache.render(config.releaseMessage, {
            version: newVersion
        });

        return gitCommit(message);
    })
    .then(function() {
        return gitTag(newVersion);
    })
    .then(function() {
        return gitPushTag(newVersion);
    })
    .then(npmPublish)
    .then(function() {
        bumpVersion(config.files, 'dev');
        return gitAdd(config.files.concat(config.readmeFile));
    })
    .then(function() {
        return gitCommit(config.backToDevMessage);
    })
    .then(function() {
        gitPush();
    })
    .then(function() {
        console.log('');
        status('Release successfully done!');
    })
    .catch(function(err) {
        console.error('\n!! Releasing failed')
        console.trace(err);
        process.exit(2);
    });
}

function parseArgs() {
    program
        .usage('bump')

    program.on('--help', function() {
        console.log('  Example usage:');
        console.log('');
        console.log('  $ ./release.js minor');
    });

    program.parse(process.argv);
}

function mergeArgsToDefaults(config) {
    if (program.args[0]) {
        config.bumpType = program.args[0];

        if (!_.contains(['major', 'minor', 'patch'], config.bumpType)) {
            console.error('Error:', config.bumpType, 'is not a valid bump type');
            process.exit(1);
        }
    }

    return config;
}

function status( /* arguments */ ) {
    var args = Array.prototype.slice.call(arguments);
    console.log(config.consolePrefix, args.join(' '));
}

function run(cmd, msg) {
    // All calls are actually synchronous but eventually some task
    // will need async stuff, so keep them promises
    return new Promise(function(resolve, reject) {
        if (msg) {
            status(msg);
        }

        if (config.dryRun) {
            return resolve();
        }

        var exec = shell.exec(cmd);
        var success = exec.code === 0;

        if (success) {
            resolve(exec.output);
        } else {
            var errMsg = 'Error executing: `' + cmd + '`\nOutput:\n' + exec.output;
            var err = new Error(errMsg);
            reject(err);
        }
    });
}

// Task functions
// All functions should return promise

// Bumps version in specified files.
// Files are assumed to contain JSON data which has "version" key following
// semantic versioning
function bumpVersion(files, bumpType) {
    status('Bump', bumpType, 'version to files:', files.join(' '));
    if (config.dryRun) return '[not available in dry run]';

    var newVersion;
    var originalVersion;
    files.forEach(function(fileName) {
        var filePath = path.join(projectRoot, fileName);

        var data = JSON.parse(fs.readFileSync(filePath));
        originalVersion = data.version;
        var currentVersion = data.version;
        if (!semver.valid(currentVersion)) {
            var msg = 'Invalid version ' + currentVersion +
                ' in file ' + fileName;;
            var err = new Error(msg);
            throw err;
        }

        if (S(currentVersion).endsWith(config.devSuffix)) {
            currentVersion = S(currentVersion).chompRight(config.devSuffix).s;
        }

        if (bumpType === 'dev') {
            newVersion = currentVersion + config.devSuffix;
        } else {
            newVersion = semver.inc(currentVersion, bumpType);
        }
        data.version = newVersion;

        var content = JSON.stringify(data, null, config.indentation);
        fs.writeFileSync(filePath, content);

        status('Bump', originalVersion, '->', newVersion, 'in',
            fileName);
    });

    bumpReadmeVersion(originalVersion, newVersion, bumpType);

    return newVersion;
}

function insertBanner(files, banner) {
    status('Insert banner to', files.length, 'files');
    if (config.dryRun) return;

    _.each(files, function(fileName) {
        var filePath = path.join(projectRoot, fileName);
        var content = fs.readFileSync(filePath);
        var newContent = banner + content;

        fs.writeFileSync(filePath, newContent);
    });
}

function bumpReadmeVersion(oldVersion, newVersion, bumpType) {
    if (bumpType === 'dev') {
        // Don't bump readme version in to dev version
        return;
    }

    var oldReleaseVersion = oldVersion;
    if (S(oldReleaseVersion).endsWith(config.devSuffix)) {
        oldReleaseVersion = S(oldReleaseVersion).chompRight(config.devSuffix).s;
    }

    status('Replace readme version', oldReleaseVersion, '->', newVersion);
    if (config.dryRun) return;

    var filePath = path.join(projectRoot, config.readmeFile);
    var content = fs.readFileSync(filePath, {encoding: 'utf-8'});

    // Update visible version
    var re = new RegExp('Version: ' + oldReleaseVersion, 'g');
    var newContent = content.replace(re, 'Version: ' + newVersion);

    // Replace link to previous stable
    re = new RegExp('tree/[0-9]\\.[0-9]\\.[0-9]');
    newContent = newContent.replace(re, 'tree/' + oldReleaseVersion);

    fs.writeFileSync(filePath, newContent);
}

function gitAdd(files) {
    var cmd = 'git add ' + files.join(' ');
    var msg = 'Staged ' + files.length + ' files';
    return run(cmd, msg);
}

function gitCommit(message) {
    var cmd = 'git commit -m "' + message + '"';
    var msg = 'Commit files'
    return run(cmd, msg);
}

function gitTag(name) {
    var cmd = 'git tag ' + name;
    var msg = 'Created a new git tag: ' + name;
    return run(cmd, msg);
}

function gitPush() {
    if (config.noPush) return;

    var cmd = 'git push';
    var msg = 'Push to remote';
    return run(cmd, msg);
}

function gitPushTag(tagName) {
    if (config.noPush) return;

    var cmd = 'git push origin ' + tagName;
    var msg = 'Push created git tag to remote'
    return run(cmd, msg);
}

function gitCheckout(branch) {
    var cmd = 'git checkout ' + branch;
    var msg = 'Checkout branch ' + branch;
    return run(cmd, msg);
}

function npmPublish() {
    if (config.noPush) return;

    var cmd = 'npm publish';
    var msg = 'Publish to npm';
    return run(cmd, msg);
}

function _gitBranchName() {
    var cmd = 'git rev-parse --abbrev-ref HEAD'
    return run(cmd, false);
}


main();
