/*!
 * Copyright 2015 Apereo Foundation (AF) Licensed under the
 * Educational Community License, Version 2.0 (the "License"); you may
 * not use this file except in compliance with the License. You may
 * obtain a copy of the License at
 *
 *     http://opensource.org/licenses/ECL-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an "AS IS"
 * BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing
 * permissions and limitations under the License.
 */

var _ = require('underscore');
var fs = require('fs');
var gift = require('gift');
var path = require('path');
var util = require('util');

var IO = require('oae-util/lib/io');
var log = require('oae-logger').logger('oae-version');

// A variable that will hold the path to the UI directory
var _uiPath = null;

// A variable that will hold a copy of the version information
var _version = null;

/**
 * Initialize the version module
 *
 * @param  {String}     uiPath  The path to the UI directory
 */
var init = module.exports.init = function(uiPath) {
    _uiPath = uiPath;
};

/**
 * Get the version information for OAE
 *
 * @param  {Function}   callback                    Standard callback function
 * @param  {Object}     callback.err                An error that occurred, if any
 * @param  {Object}     callback.version            The version information
 * @param  {String}     callback.version.hilary     The version information for Hilary
 * @param  {String}     callback.version.3akai-ux   The version information for the UI
 */
var getVersion = module.exports.getVersion = function(callback) {
    if (_version) {
        return callback(null, _version);
    }

    _getHilaryVersion(function(err, hilaryVersion) {
        if (err) {
            return callback(err);
        }

        _getUIVersion(function(err, uiVersion) {
            if (err) {
                return callback(err);
            }

            _version = {
                'hilary': hilaryVersion,
                '3akai-ux': uiVersion
            };
            return callback(null, _version);
        });
    });
};

/**
 * Get the version information for the backend
 *
 * @param  {Function}   callback            Standard callback function
 * @param  {Object}     callback.err        An error that occurred, if any
 * @param  {String}     callback.version    The version information for the backend
 * @api private
 */
function _getHilaryVersion(callback) {
    var hilaryDirectory = path.resolve(__dirname, '..', '..', '..');
    _getVersionInfo(hilaryDirectory, callback);
}

/**
 * Get the version information for the UI
 *
 * @param  {Function}   callback            Standard callback function
 * @param  {Object}     callback.err        An error that occurred, if any
 * @param  {String}     callback.version    The version information for the UI
 * @api private
 */
function _getUIVersion(callback) {
    _getVersionInfo(_uiPath, callback);
}

/**
 * Get the version information for a directory. The version will be retrieved from
 * the build info file in the root of the directory. If there's no such file present
 * or it can't be parsed, the information will be retrieved from git. If that fails
 * as well `Unknown` will be returned
 *
 * @param  {String}     directory           The directory to get the version information for
 * @param  {Function}   callback            Standard callback function
 * @param  {Object}     callback.err        An error that occurred, if any
 * @param  {String}     callback.version    The version information for the directory
 * @api private
 */
function _getVersionInfo(directory, callback) {
    _getBuildInfoFile(directory, function(err, buildInfoPath) {
        if (err) {
            return callback(err);
        } else if (buildInfoPath) {
            return _getBuildVersion(buildInfoPath, callback);
        } else {
            return _getGitVersion(directory, callback);
        }
    });
}

/**
 * Each generated build has a `build-info.json` file that contains information about the build.
 * Parse it and return the relevant version information
 *
 * @param  {String}     buildInfoPath       The path to the build info file
 * @param  {Function}   callback            Standard callback function
 * @param  {Object}     callback.err        An error that occurred, if any
 * @param  {String}     callback.version    The version information for the directory
 * @api private
 */
function _getBuildVersion(buildInfoPath, callback) {
    fs.readFile(buildInfoPath, function(err, buildInfo) {
        if (err) {
            return callback({'code': 500, 'msg': 'Unable to read the build info file'});
        }

        try {
            buildInfo = JSON.parse(buildInfo);
        } catch (ex) {
            log().error({
                'err': ex,
                'path': buildInfoPath
            }, 'Unable to parse the build info file');
            return callback({'code': 500, 'msg': 'Unable to parse the build info file'});
        }
        buildInfo.type = 'archive';

        return callback(null, buildInfo);
    });
}

/**
 * Get the version from git
 *
 * @param  {String}     directory           The directory to the git repository
 * @param  {Function}   callback            Standard callback function
 * @param  {Object}     callback.err        An error that occurred, if any
 * @param  {String}     callback.version    The version information for the directory
 * @api private
 */
function _getGitVersion(directory, callback) {
    gift.init(directory, function(err, repo) {
        if (err) {
            log().error({
                'err': err,
                'path': directory
            }, 'Could not open the git repo to get the version information');
            return callback({'code': 500, 'msg': 'Could not open the git repo'});
        }

        // Get the tag info
         repo.tags(function(err, tags) {
            if (err) {
                log().error({
                    'err': err,
                    'path': directory
                }, 'Could not get the git tags');
                return callback({'code': 500, 'msg': 'Could not get the git tags'});
            }

            var tagsByCommitId = _.indexBy(tags, function(tag) {
                return tag.commit.id;
            });

            var hasTriedSubmodule = false;
            var innerCallback = function(err, branch) {
                if (err) {
                    // 3akai-ux might be a submodule so we need to try that
                    if (repo.path === _uiPath && !hasTriedSubmodule) {
                        log().warn('Tried fetching git branch information as a regular repo, will try as a submodule next');
                        var pathElements = repo.dot_git.split('/');
                        var dot_git = pathElements.pop();
                        var submodule = pathElements.pop();
                        pathElements.push(dot_git, 'modules', submodule);
                        repo.dot_git = pathElements.join('/');

                        hasTriedSubmodule = true;
                        return repo.branch(innerCallback);
                    }

                    log().error({
                        'err': err,
                        'path': directory
                    }, 'Could not get the current git branch for the version information');
                    return callback({'code': 500, 'msg': 'Could not get the branch information'});
                }

                // Get the number of commits since the last tag
                _getGitDescribeVersion(repo, branch.name, tagsByCommitId, function(err, describeVersion) {
                    if (err) {
                        log().error({
                            'err': err,
                            'path': directory
                        }, 'Could not get the git describe version for a repo');
                        return callback({'code': 500, 'msg': 'Could not get the branch information'});
                    }

                    var buildInfo = {
                        'branch': branch.name,
                        'date': branch.commit.committed_date,
                        'type': 'git',
                        'version': describeVersion
                    };
                    return callback(null, buildInfo);
                });
            };

            // Get the current branch info
            repo.branch(innerCallback);
        });
    });
}

/**
 * Get a "git describe"-like descriptor for a branch.
 *
 * This will return information in the form:
 *   <last tag>+<number of commits>+<sha1 of last commit>
 *
 * @param  {Repository}     repo                The git repository to get the information for
 * @param  {String}         branchName          The name of the branch to get the information for
 * @param  {Object}         tagsByCommitId      The Tag objects keyed by their commit sha1 hash
 * @param  {Function}       callback            Standard callback function
 * @param  {Object}         callback.err        An error that occurred, if any
 * @param  {String}         callback.version    The git describe information
 * @api private
 */
function _getGitDescribeVersion(repo, branchName, tagsByCommitId, callback, _nrOfCommits, _lastCommitSha) {
    _nrOfCommits = _nrOfCommits || 0;
    repo.commits(branchName, 30, _nrOfCommits, function(err, commits) {
        if (err) {
            return callback(err);
        }

        // Retain the sha1 of the last commit on this branch
        _lastCommitSha = _lastCommitSha || commits[0].id;

        // Try to find the last tagged commit in these set of commits
        var lastTag = null;
        for (var i = 0; i < commits.length; i++) {
            if (tagsByCommitId[commits[i].id]) {
                lastTag = tagsByCommitId[commits[i].id].name;
                break;
            }
            _nrOfCommits++;
        }

        // If we found a tag in the set of commits we can return the git describe information
        if (lastTag) {
            var describeVersion = util.format('%s+%d+%s', lastTag, _nrOfCommits, _lastCommitSha);
            return callback(null, describeVersion);

        // Otherwise we need to retrieve the next set of commits
        } else {
            return _getGitDescribeVersion(repo, branchName, tagsByCommitId, callback, _nrOfCommits, _lastCommitSha);
        }
    });
}
/**
 * Check whether a directory has a build-info.json file. This function
 * will recursively check each parent directory as well.
 *
 * @param  {String}     directory                       The directory to get the version information for
 * @param  {Function}   callback                        Standard callback function
 * @param  {Object}     callback.err                    An error that occurred, if any
 * @param  {String}     [callback.buildInfoPath]        The path to the build-info.json file. `null` if no such file could be found
 * @api private
 */
function _getBuildInfoFile(directory, callback) {
    var buildInfoPath = _getBuildInfoFilePath(directory);
    IO.exists(buildInfoPath, function(err, exists) {
        if (err) {
            log.error({
                'directory': directory,
                'err': err
            }, 'Unable to check if a build info file exists');
            return callback(err);
        } else if (exists) {
            return callback(null, buildInfoPath);
        } else {
            var parent = path.dirname(directory);
            if (parent !== directory) {
                return _getBuildInfoFile(parent, callback);
            } else {
                return callback();
            }
        }
    });
}

/**
 * Get the path to the `build-info.json` file
 *
 * @param  {String}     directory       The directory to get the version information for
 * @return {String}                     The path to the `build-info.json` file
 * @api private
 */
function _getBuildInfoFilePath(directory) {
    return path.join(directory, 'build-info.json');
}
