/**
 * git-control.js
 * Copyright (c) 2000 - 2017 Samsung Electronics Co., Ltd. All rights reserved.
 *
 * Contact:
 * Sungmin Kim <sm.art.kim@samsung.com>
 * Jonghwan Park <iwin100.park@samsung.com>
 * Kitae Kim <kt920.kim@samsung.com>
 *
 * Licensed under the Apache 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://www.apache.org/licenses/LICENSE-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.
 *
 * Contributors:
 * - S-Core Co., Ltd
**/

/**
 * Tizen git control module
 * @module models/tizen-project/git-control
 */

var path = require('path');
var async = require('async');
var extfs = require('fs-extra');
var fs = require('fs');
var _ = require('underscore');
var LockFile = require('lockfile');

var utils = require('../../lib/utils.js');
var Git = require('../dibs.scm.git/git.js');
var FileSystem = require('../dibs.core/filesystem.js');


function acquireLock(lockFile, callback) {
    if (lockFile) {
        LockFile.lock(lockFile, {wait: 1000 * 60 * 30}, function (err) {
            callback(err);
        });
    } else {
        callback(null);
    }
}


function releaseLock(lockFile) {
    if (lockFile) {
        LockFile.unlockSync(lockFile);
    }
}


module.exports.getSourceCode = function (job, options, callback) {
    var jobWorkPath = options.jobWorkPath;
    var workspacePath = options.workspacePath;
    var lockFilePath = options.lockFilePath;
    var monitor = options.monitor;

    var jobSourcePath = null;
    var isGitCacheValid = false;
    var projectPath = path.join(workspacePath, 'projects',
        job.distName, job.projectName);
    var lockFile = path.join(lockFilePath, job.distName + job.projectName + '.lock');

    // if project's git-cache not exists, clone it
    var projectGitCachePath = path.join(projectPath, 'git-cache');
    async.series([
        function (cb) {
            acquireLock(lockFile, cb);
        },
        function (cb) {
            // create project path, if not exist
            fs.exists(projectPath, function (exists) {
                if (!exists) {
                    extfs.mkdirs(projectPath, function (err) {
                        cb(err);
                    });
                } else {
                    cb(null);
                }
            });
        },
        function (cb) {
            monitor.updateProgress('Checking GIT cache...', cb);
        },
        function (cb) { // verify
            job.gitRepo = job.options.GIT_REPO;
            fs.exists(projectGitCachePath, function (exists) {
                if (exists) {
                    Git.configGet('remote.origin.url', {
                        workPath: projectGitCachePath
                    }, function (err, value) {
                        if (err) {
                            monitor.updateProgress(' - Failed to get \'remote.origin.url\' value from ' + projectGitCachePath);
                            cb(null);
                        } else {
                            if (value === job.options.GIT_REPO) {
                                isGitCacheValid = true;
                                monitor.updateProgress(' - GIT cache is valid!', cb);
                                return;
                            } else {
                                monitor.updateProgress(' - GIT cache is invalid! Removing cache...');
                                FileSystem.remove(projectGitCachePath, cb);
                            }
                        }
                    });
                } else {
                    monitor.updateProgress(' - git cache directory does not exist.', cb);
                }
            });
        },
        function (cb) { // git clone
            if (isGitCacheValid) {
                job.options.GIT_CACHED = true;
                useGitCache(job, projectGitCachePath, monitor, cb);
            } else {
                job.options.GIT_CACHED = false;
                useGitClone(job, projectGitCachePath, monitor, cb);
            }
        },
        function (cb) {
            monitor.updateProgress('Checking GIT/Gerrit Patchset/Github Pull-Request build option...', cb);
        },
        function (cb) {
            if (job.options.GIT_PATCHSET) {
                monitor.updateProgress(' - Use gerrit patchset \'' + job.options.GIT_PATCHSET + '\'');
                usePatchset(job, projectGitCachePath, monitor, cb);
            } else if (job.options.GIT_COMMIT) {
                job.gitCommit = job.options.GIT_COMMIT;
                job.options.GIT_CURRENT_COMMIT = job.gitCommit;
                monitor.updateProgress(' - git reset --hard ' + job.options.GIT_COMMIT);
                Git.reset(job.options.GIT_COMMIT, {
                    workPath: projectGitCachePath
                }, cb);
            } else if (job.options.GITHUB_PULLREQUEST) {
                monitor.updateProgress(' - Use github pull-request \'' + job.options.GITHUB_PULLREQUEST + '\'');
                job.ghpr = job.options.GITHUB_PULLREQUEST;
                useGithubPullRequest(job, projectGitCachePath, monitor, cb);
            } else {
                monitor.updateProgress(' - git log -1');
                Git.getCommitId(projectGitCachePath, function (err, commitId) {
                    if (!err) {
                        job.gitCommit = commitId;
                        job.options.GIT_CURRENT_COMMIT = commitId;
                    }
                    cb(err);
                });
            }
        },
        function (cb) {
            if (job.options.USE_GIT_SUBMODULES) {
                useGitSubmodule(projectGitCachePath, monitor, cb);
            } else {
                monitor.updateProgress('Skipping GIT submodule...');
                cb(null);
            }
        },
        function (cb) {
            monitor.updateProgress('Copying GIT cache to job directory...', cb);
        },
        // copy
        function (cb) {
            // retry copying git source to job work path until 5 times
            var retryCount = 0;
            jobSourcePath = path.join(jobWorkPath, 'source');
            async.retry({
                times: 5,
                interval: 30 * 1000
            }, function (cb1) {
                retryCount++;
                copyGitSourceToJobWorkPath(projectGitCachePath, jobWorkPath, jobSourcePath, 0, monitor, cb1);
            }, function (err1) {
                if (err1) {
                    monitor.updateProgress(' - Failed to copy project source to job directory... retrying... ' + retryCount);
                }
                cb(err1);
            });
        }
    ], function (err) {
        releaseLock(lockFile);
        callback(err, jobSourcePath);
    });
};


function removeAndCloneAgain(job, projectGitCachePath, monitor, callback) {
    async.series([
        function (cb) {
            monitor.updateProgress(' - removing GIT cache...', cb);
        },
        function (cb) {
            FileSystem.remove(projectGitCachePath, cb);
        },
        function (cb) {
            monitor.updateProgress(' - git clone ' + job.options.GIT_REPO + ' -b ' + job.options.GIT_BRANCH, cb);
        },
        function (cb) {
            clone(job.options.GIT_REPO, {
                gitBranch: job.options.GIT_BRANCH,
                targetDir: projectGitCachePath,
                onStdout: function (string) {
                    monitor.updateProgress(string);
                },
                onStderr: function (string) {
                    monitor.updateProgress(string);
                }
            }, cb);
        }
    ],
    function (err, options) {
        if (job.status === 'INITIALIZING') {
            job.options.GIT_RETRY_COUNT_INIT = options.gitRetry;
        } else {
            job.options.GIT_RETRY_COUNT_BUILD = options.gitRetry;
        }
        callback(err);
    });

}

function clone(repo, options, callback) {
    Git.clone(repo, options, function (err, targetDir) {
        if (!options.gitRetry) {
            options.gitRetry = 0;
        }
        options.targetDir = targetDir;
        if (err) {
            if (options.gitRetry <= 10) {
                utils.removePathIfExist(options.targetDir, function (err) {
                    if (err) {
                        callback(err, null);
                    } else {
                        setTimeout(function () {
                            options.gitRetry = options.gitRetry + 1;
                            clone(repo, options, callback);
                        }, 1000);
                    }
                });
            } else {
                callback(err, options);
            }
        } else {
            callback(err, options);
        }
    });
}


function useGitCache(job, projectGitCachePath, monitor, callback) {
    // fetch existing GIT & reset --hard origin/branch
    async.series([
        function (cb) {
            monitor.updateProgress('Fetching to GIT cache...', cb);
        },
        function (cb) {
            monitor.updateProgress(' - git fetch origin', cb);
        },
        function (cb) {
            Git.fetch('origin', null, {
                workPath: projectGitCachePath
            }, cb);
        },
        function (cb) {
            monitor.updateProgress(' - git reset --hard origin/' + job.options.GIT_BRANCH, cb);
        },
        function (cb) {
            Git.reset('origin/' + job.options.GIT_BRANCH, {
                workPath: projectGitCachePath
            }, cb);
        }
    ],
    function (err) {
        if (err) {
            monitor.updateProgress(' - fetching failed... retrying...');
            removeAndCloneAgain(job, projectGitCachePath, monitor, callback);
        } else {
            callback(null);
        }
    });
}


function useGitClone(job, projectGitCachePath, monitor, callback) {
    monitor.updateProgress('Cloning into GIT cache...');
    monitor.updateProgress(' - git clone ' + job.options.GIT_REPO + ' -b ' + job.options.GIT_BRANCH);

    clone(job.options.GIT_REPO, {
        gitBranch: job.options.GIT_BRANCH,
        targetDir: projectGitCachePath,
        onStdout: function (string) {
            monitor.updateProgress(string);
        },
        onStderr: function (string) {
            monitor.updateProgress(string);
        }
    }, function (err, options) {
        if (job.status === 'INITIALIZING') {
            job.options.GIT_RETRY_COUNT_INIT = options.gitRetry;
        } else {
            job.options.GIT_RETRY_COUNT_BUILD = options.gitRetry;
        }
        callback(err);
    });
}


function usePatchset(job, projectGitCachePath, monitor, callback) {
    async.series([
        function (cb) {
            monitor.updateProgress(' - git fetch ' +
                job.options.GIT_REPO + ' ' + job.options.GIT_PATCHSET);
            Git.fetch(job.options.GIT_REPO, job.options.GIT_PATCHSET, {
                workPath: projectGitCachePath
            }, cb);
        },
        function (cb) {
            monitor.updateProgress(' - git checkout FETCH_HEAD');
            Git.checkout('FETCH_HEAD', {
                workPath: projectGitCachePath
            }, cb);
        },
        function (cb) {
            monitor.updateProgress(' - git log -1');
            Git.getCommitId(projectGitCachePath, function (err, commitId) {
                if (!err) {
                    job.gitCommit = commitId;
                    job.options.GIT_CURRENT_COMMIT = commitId;
                }
                cb(err);
            });
        }
    ], callback);
}


function useGitSubmodule(projectGitCachePath, monitor, callback) {
    monitor.updateProgress('Use git submodule ... ' + projectGitCachePath);

    async.retry({
        times: 3,
        interval: 30 * 1000
    },
    function (cb) {
        Git.submodule('update', [ '--init' ],
            {
                workPath: projectGitCachePath,
                onStdout: function (string) {
                    monitor.updateProgress(string);
                },
                onStderr: function (string) {
                    monitor.updateProgress(string);
                }
            },
            function (err1) {
                if (err1) {
                    monitor.updateProgress('- git submodule update ... failure');
                } else {
                    monitor.updateProgress('- git submodule update ... done');
                }
                cb(err1, null);
            });
    }, function (err, results) {
        callback(err, results);
    });
}


function useGithubPullRequest(job, projectGitCachePath, monitor, callback) {
    var commitId;

    async.series([
        function (cb) {
            /*
             * git fetch
             * ex) git fetch --tags --progress {REPO} +refs/pull/*:refs/remotes/origin/pr/*
             */
            var fetchOpts = ['--tags', '--progress', '+refs/pull/*:refs/remotes/origin/pr/*'];
            monitor.updateProgress(' - git fetch ' +
                job.options.GIT_REPO);
            Git.fetch(job.options.GIT_REPO, null, {
                workPath: projectGitCachePath,
                fetchOpts: fetchOpts
            }, cb);
        },
        function (cb) {
            // git rev-parse
            var refSpec = '';
            var pr = job.options.GITHUB_PULLREQUEST;
            if (pr.match(/\b[0-9a-f]{5,40}\b/)) {
                // sha1
                refSpec = pr;
            } else if (pr.match(/[\d]/)) {
                // PR number
                refSpec = 'refs/remotes/origin/pr/' + job.options.GITHUB_PULLREQUEST + '/merge';
            } else {
                // nothing
            }

            monitor.updateProgress(' - git rev-parse ' + refSpec);
            Git.revParse(refSpec, {
                workPath: projectGitCachePath
            }, function (err, result) {
                commitId = result;
                cb(err);
            });
        },
        function (cb) {
            monitor.updateProgress(' - git checkout ' + commitId);
            Git.checkout(commitId, {
                workPath: projectGitCachePath,
                checkoutOpts: ['-f']
            }, cb);
        },
        function (cb) {
            monitor.updateProgress(' - git log -1');
            Git.getCommitId(projectGitCachePath, function (err, commitId) {
                if (!err) {
                    job.gitCommit = commitId;
                    job.options.GIT_CURRENT_COMMIT = commitId;
                }
                cb(err);
            });
        }
    ], callback);
}

function copyGitSourceToJobWorkPath(projectGitCachePath, jobWorkPath, jobSourcePath, retryCount, monitor, callback) {
    async.series([
        function (cb) {
            fs.exists(jobSourcePath, function (exist) {
                if (exist) {
                    extfs.remove(jobSourcePath, cb);
                } else {
                    cb(null);
                }
            });
        },
        function (cb) {
            extfs.mkdirs(jobSourcePath, cb);
        },
        function (cb) {
            monitor.updateProgress(' - copy source from ' + projectGitCachePath + ' to ' + jobSourcePath, cb);
        },
        function (cb) {
            copyGitSource(projectGitCachePath, jobSourcePath, cb);
        }
    ], callback);
}


function copyGitSource(source, target, callback) {
    var sourceDirList = [];

    async.waterfall([
        function (cb) {
            fs.readdir(source, function (err1, data) {
                sourceDirList = data;
                cb(err1);
            });
        },
        function (cb) {
            fs.stat(path.join(source, '.git'), function (err1, stats) {
                if (err1) {
                    cb(err1, false);
                } else {
                    cb(null, stats.isDirectory());
                }
            });
        },
        function (isDir, cb) {
            var results = null;
            if (isDir) {
                results = _.filter(sourceDirList, function (file) {
                    return (file !== '.git');
                });
            } else {
                results = sourceDirList;
            }

            async.eachLimit(results, 4,
                function (file, cb1) {
                    FileSystem.copy(path.join(source, file), path.join(target, file), cb1);
                },
                function (err1) {
                    cb(err1);
                });
        }
    ],
    function (err) {
        callback(err);
    });
}


module.exports.getLog = function (jobWorkPath, since, until, callback) {
    var commitLog = [];
    var index = -1;
    var contents = '';
    Git.getlog(jobWorkPath, since, until, function (err, log) {
        if (!err) {
            _.each(log.split('\n'), function (string) {
                if (string.match(/commit [a-z0-9]+/)) {
                    if (contents !== '') {
                        commitLog[index] = _.extend(commitLog[index], {
                            Contents: contents
                        });
                    }
                    contents = '';
                    index++;
                    commitLog[index] = {
                        Commit: string.split(' ')[1]
                    };
                } else if (string.match(/Author:.*<.*>/)) {
                    commitLog[index] = _.extend(commitLog[index], {
                        Author: _.rest(string.split(':')).join(':').trim()
                    });
                } else if (string.match(/Date:.* [0-2][0-9]:[0-5][0-9]:[0-5][0-9] .*/)) {
                    commitLog[index] = _.extend(commitLog[index], {
                        Date: _.rest(string.split(':')).join(':').trim()
                    });
                } else {
                    if (contents === '') {
                        contents += string;
                    } else {
                        contents += '\n' + string;
                    }
                }
            });
            if (contents !== '') {
                commitLog[index] = _.extend(commitLog[index], {
                    Contents: contents
                });
            }
        }
        callback(err, commitLog);
    });
};
