/**
 * ts-build.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
 **/

'use strict';

require('draftlog').into(console);

var async = require('async');
var chalk = require('chalk');
var extfs = require('fs-extra');
var fs = require('fs');
var os = require('os');
var path = require('path');
var windowSize = require('window-size');
var winston = require('winston');
var _ = require('underscore');

var util = require('../../org.tizen.ts.base.common/util.js');
var utils = require('../../../lib/utils.js');
var TsRepo = require('../../org.tizen.ts.cli.repository/ts-repo.js');
var TsJob = require('../../org.tizen.ts.cli.projects/ts-job.js');

var startTime = new Date();
var progressList = [];

module.exports.build = function (argv, log) {
    //for job log object
    process.setMaxListeners(0);

    var hostOs = util.getTargetOS(os.platform(), os.arch());

    var options = {};
    options.force = argv.f;
    options.TARGET_OS = argv.o || hostOs.os;
    options.push = true;
    options.cloneSource = argv.cs;

    var repository = argv.r;
    var cwd = argv.s;
    var exclude = argv.x;


    if (!repository) {
        log.error('Usage: ts-cli full-build --repository <repository path> [--source <source path>] [--exclude <exclude path list>]');
        process.exit(-1);
    } else {
        repository = path.resolve(repository);
    }

    if (exclude) {
        options.exclude = exclude.split(',');
    } else {
        options.exclude = [];
    }

    log.info('Tizen Studio Full Build Process - Start');

    var repo = new TsRepo({
        location: repository
    });

    async.waterfall([
        function (cb) {
            repo.load(cb);
        },
        function (cb) {
            init(repo, cwd, log, options, cb);
        },
        function (jobs, cb) {
            schedule(repo, jobs, log, options, function (err1) {
                if (err1) {
                    log.error('Canceled Full-Build process by Error');
                    _.each(err1, function (e) {
                        log.error(' > ' + e.name);
                        log.error(' >> ' + e.error);
                    });
                    process.exit(-1);
                }
                cb(null);
            });
        }
    ], function (err) {
        if (err) {
            log.error(err);
            process.exit(-1);
        }
        process.exit(0);
    });
};

function padStart(str, len, pad) {
    return pad.repeat(len - str.length) + str;
}

function padEnd(str, len, pad) {
    return str + pad.repeat(len - str.length);
}

function timeDiff(start) {
    var now = new Date();
    var diff = new Date(now.getTime() - start.getTime());
    var hour = diff.getHours();
    var min = diff.getMinutes();
    var sec = diff.getSeconds();
    var millisec = diff.getMilliseconds();
    if (sec === 0 && millisec > 0) {
        sec = sec + 1;
    }
    return padStart(hour.toString(), 2, '0') + ':' + padStart(min.toString(), 2, '0') + ':' + padStart(sec.toString(), 2, '0');
}

function progress(jobs, finishJob, workingQueue) {
    var size = windowSize.get();
    var total = jobs.length;

    if (_.isEmpty(progressList)) {
        progressList[0] = console.draft();
    }

    // increase progress line
    var boardLine = progressList.length - 1; // for last status line
    if (progressList.length < size.height - 1 && boardLine < workingQueue.length) {
        var max = (size.height - 1) - progressList.length;
        var increase = workingQueue.length - boardLine;
        var min = Math.min(max, increase);
        var length = progressList.length;
        for (var i = length; i < (length + min); i++) {
            progressList[i] = console.draft();
        }
    }

    boardLine = progressList.length - 1;

    if (workingQueue.length > boardLine) {
        var deleteCnt = workingQueue.length - boardLine;
        var idx = 0;
        workingQueue = _.reject(workingQueue, function (job) {
            if (job.status === 'WORKING' || job.status === 'ERROR') {
                return false;
            } else if (idx < deleteCnt) {
                idx++;
                return true;
            } else {
                return false;
            }
        });
    }

    i = 0;
    _.each(workingQueue, function (job) {
        if (job && job.status === 'WORKING') {
            progressList[i](chalk.cyan(' > ') + chalk.yellow.dim(padEnd(job.name, 50, ' ')) + chalk.green('[' + timeDiff(job.startTime) + ']') + '    ' + chalk.green(job.status));
        } else if (job && job.status === 'ERROR') {
            progressList[i](chalk.cyan(' > ') + chalk.yellow.dim(padEnd(job.name, 50, ' ')) +
                chalk.green('[' + job.time + ']') + '    ' + chalk.red(job.status) + '     ' + chalk.grey('[LOG FILE: ' + job.logPath + ']'));
        } else if (job && job.status === 'SUCCESS') {
            progressList[i](chalk.cyan(' > ') + chalk.yellow.dim(padEnd(job.name, 50, ' ')) +
                chalk.green('[' + job.time + ']') + '    ' + chalk.blue(job.status));
        } else {
            progressList[i]('');
        }
        i++;
    });

    progressList[progressList.length - 1](chalk.yellow(' ===   Total: ') + chalk.green(padStart(total.toString(), 4, ' ')) +
        chalk.yellow('  Finish: ') + chalk.green(padStart(finishJob.toString(), 4, ' ')) +
        chalk.yellow('  Time:   ') + chalk.green(timeDiff(startTime)) +
        chalk.yellow('   ==='));
}

function init(repo, cwd, log, options, callback) {
    var distribution = repo.distributions[repo.distName];
    var snapshot = distribution.latestSnapshot;
    async.waterfall([
        function (cb) {
            fs.readdir(cwd, cb);
        },
        function (files, cb) {
            createJob(cwd, files, log, options, cb);
        },
        function (jobs, cb) {
            filterOs(jobs, log, options, cb);
        },
        function (jobs, cb) {
            checkDependency(distribution, snapshot, jobs, log, options, cb);
        },
        function (jobs, cb) {
            setWorkspace(cwd, jobs, cb);
        }
    ], function (err, jobs) {
        if (err) {
            return callback(err);
        } else if (_.isEmpty(jobs)) {
            return callback('Nothing jobs');
        } else {
            log.info('BUILD LIST: ');
            _.each(jobs, function (job) {
                log.info(' > ' + job.name + ' [' + job.conflict.toString() + ']');
            });
            return callback(null, jobs);
        }
    });
}

function enableConsoleLog(log) {
    log.add(winston.transports.Console, {
        handleExceptions: true,
        colorize: true,
        formatter: function (opts) {
            return winston.config.colorize(opts.level, opts.level.toUpperCase() + ' ' + (opts.message ? opts.message : '') +
                (opts.meta && Object.keys(opts.meta).length ? '\n\t' + JSON.stringify(opts.meta) : ''));
        },
        level: 'info'
    });
}

function schedule(repo, jobs, log, options, callback) {
    var queue = _.clone(jobs);
    var workingQueue = [];
    var scheduledQueue = [];
    var worker = [];
    var max = 3;
    var error = [];
    var finishJob = 0;

    log.info('Start schedule');

    // Disable Console log
    log.remove(winston.transports.Console);

    var loop = setInterval(
        function () {
            progress(jobs, finishJob, workingQueue);

            _.each(queue, function (job) {
                if (!job.status && _.isEmpty(job.conflict)) {
                    queue = popQueue(queue, job.name);
                    scheduledQueue.push(job);
                    job.status = 'WAIT';
                    log.info('Dispatched Scheduling table... [WAIT]' + job.name);
                }
            });

            if (_.isEmpty(queue) && _.isEmpty(scheduledQueue) && _.isEmpty(worker)) {
                clearInterval(loop);
                enableConsoleLog(log);
                return callback(null);
            } else if (!_.isEmpty(error)) {
                if (_.isEmpty(worker)) {
                    // Stop Error
                    clearInterval(loop);
                    enableConsoleLog(log);
                    return callback(error);
                } else {
                    // Wait working jobs
                    return;
                }
            } else if (worker.length < max) {
                while (worker.length < max && !_.isEmpty(scheduledQueue)) {
                    var job = scheduledQueue.pop();
                    worker.push(job);
                    job.startTime = new Date();
                    job.status = 'WORKING';
                    workingQueue.push(job);
                    log.info('Start build... [WORKING]' + job.name);

                    build(repo, job, options, function (err, finJob) {
                        finJob.endTime = new Date();
                        if (err) {
                            finJob.status = 'ERROR';
                            finJob.time = timeDiff(finJob.startTime);
                            error.push({
                                name: finJob.name,
                                error: err
                            });
                            log.error('Failed build... [ERROR]' + finJob.name + ' Duration: ' + finJob.time);
                            log.error('Log file: ' + finJob.logPath);
                        } else {
                            finJob.status = 'SUCCESS';
                            finJob.time = timeDiff(finJob.startTime);
                            log.info('Success build... [SUCCESS]' + finJob.name + ' Duration: ' + finJob.time);
                            // Remove conflict
                            _.each(queue, function (j) {
                                if (finJob.type === 'Tizen-Source') {
                                    j.conflict = _.difference(j.conflict, finJob.packages);
                                } else {
                                    j.conflict = _.without(j.conflict, finJob.name);
                                }
                            });
                        }
                        worker = _.reject(worker, function (j) {
                            return j.name === finJob.name;
                        });
                        finishJob++;
                    });
                }
            } else {
                _.each(worker, function (job) {
                    if (job.progress) {
                        job.progress(chalk.cyan(' > ') + chalk.yellow.dim(padEnd(job.name, 50, ' ')) + chalk.green('[' + timeDiff(job.startTime) + ']') + '    ' + chalk.green(job.status));
                    }
                });
            }
        }, 1000);
}

function popQueue(queue, name) {
    return _.filter(queue, function (job) {
        return job.name !== name;
    });
}

function build(repo, job, options, callback) {
    try {
        job.build(repo, options, function (err) {
            callback(err, job);
        });
    } catch (e) {
        return callback(e, job);
    }
}

function filterOs(jobs, log, options, callback) {
    var filteredJobs = [];
    _.each(jobs, function (job) {
        if (job.pkgInfo && job.pkgInfo.packages) {
            var incluedBuildPackage = [];
            _.each(job.pkgInfo.packages, function (pkg) {
                if (_.indexOf(pkg.osList, options.TARGET_OS) !== -1) {
                    incluedBuildPackage.push(pkg);
                    if (job.type === 'Tizen-Source' && pkg.buildHostOS !== options.TARGET_OS) {
                        log.warn(' Build Host OS is not matched - ' + pkg.name + '(' + pkg.buildHostOS + ')' + ' ==> ' + job.name);
                    }
                }
            });

            if (!_.isEmpty(incluedBuildPackage)) {
                filteredJobs.push(job);
            }
        } else {
            filteredJobs.push(job);
        }
    });
    callback(null, filteredJobs);
}

function checkDependency(distribution, snapshot, jobs, log, options, callback) {
    var archivePath = path.join(distribution.path, 'source');
    var allPackages = [];
    var dependency = [];
    var duplicatePackages = [];
    var registeredPackages = [];
    var skipJobs = [];
    var filteredJobs = [];

    _.each(jobs, function (job) {
        var registered = false;
        if (job.type === 'Tizen-Source') {
            var duplicate = [];

            _.each(job.packages, function (pkgName) {
                if (snapshot && snapshot.osPackages[job.os] && snapshot.osPackages[job.os][pkgName]) {
                    var registeredPackage = snapshot.osPackages[job.os][pkgName];
                    if (util.package.compareVersion(job.version, registeredPackage.version) <= 0) {
                        registered = true;
                        registeredPackages.push(pkgName);
                    }
                }

                if (allPackages.indexOf(pkgName) !== -1) {
                    duplicate.push(pkgName);
                } else {
                    allPackages.push(pkgName);
                }
            });

            if (registered && !options.force) {
                skipJobs.push(job);
                return;
            } else {
                filteredJobs.push(job);

                if (!_.isEmpty(duplicate)) {
                    duplicatePackages = _.union(duplicatePackages, duplicate);
                }
            }
        } else if (job.type === 'Tizen-Binary') {
            if (snapshot && snapshot.osPackages[job.os] && snapshot.osPackages[job.os][job.name]) {
                var registeredPackage = snapshot.osPackages[job.os][job.name];
                if (util.package.compareVersion(job.version, registeredPackage.version) <= 0) {
                    registered = true;
                    registeredPackages.push(job.name);
                }
            }

            if (registered && !options.force) {
                allPackages.push(job.name);
                skipJobs.push(job);
                return;
            } else {
                filteredJobs.push(job);

                if (allPackages.indexOf(job.name) !== -1) {
                    duplicatePackages.push(job.name);
                } else {
                    allPackages.push(job.name);
                }
            }

        } else if (job.type === 'Tizen-Archive') {
            if (snapshot && snapshot.archivePackages && snapshot.archivePackages.indexOf(job.name) !== -1) {
                if (utils.getCheckSum(job.path) === utils.getCheckSum(path.join(archivePath, job.name))) {
                    registered = true;
                    registeredPackages.push(job.name);
                }
            }

            if (registered && !options.force) {
                allPackages.push(job.name);
                skipJobs.push(job);
                return;
            } else {
                filteredJobs.push(job);

                if (allPackages.indexOf(job.name) !== -1) {
                    duplicatePackages.push(job.name);
                } else {
                    allPackages.push(job.name);
                }
            }
        }

        job.buildDepList = [];
        job.installDepList = [];
        job.srcDepList = [];
        job.conflict = [];
        if (job.type === 'Tizen-Source' || job.type === 'Tizen-Binary') {
            _.each(job.pkgInfo.packages, function (pkg) {
                if (_.indexOf(pkg.osList, options.TARGET_OS) !== -1) {
                    job.buildDepList = _.union(job.buildDepList, _.pluck(pkg.buildDepList, 'packageName'));
                    job.installDepList = _.union(job.installDepList, _.pluck(pkg.installDepList, 'packageName'));
                    job.srcDepList = _.union(job.srcDepList, _.pluck(pkg.srcDepList, 'packageName'));
                    job.conflict = _.difference(_.union(job.conflict, job.buildDepList, job.installDepList, job.srcDepList), job.packages);
                    dependency = _.union(dependency, job.buildDepList, job.installDepList, job.srcDepList);
                }
            });
        }
    });

    if (!_.isEmpty(duplicatePackages)) {
        log.error('Duplicate packages exist...');
        _.each(duplicatePackages, function (pkgName) {
            log.error(' - package: ' + pkgName);
            _.each(jobs, function (job) {
                if (job.type === 'Tizen-Source') {
                    if (job.packages.indexOf(pkgName) !== -1) {
                        log.error('  => ' + job.srcPath + '(' + job.packages.toString() + ')');
                    }
                } else if (job.type === 'Tizen-Binary' || job.type === 'Tizen-Archive') {
                    if (job.name === pkgName) {
                        log.error('  => ' + job.path + '(' + job.name + ')');
                    }
                }
            });
        });
        return callback('Duplicate packages: ' + duplicatePackages.toString());
    }

    var needPkgs = [];
    _.each(dependency, function (pkgName) {
        if (allPackages.indexOf(pkgName) === -1) {
            needPkgs.push(pkgName);
        }
    });

    _.each(filteredJobs, function (job) {
        job.conflict = _.difference(job.conflict, registeredPackages);
    });

    if (_.isEmpty(needPkgs)) {
        if (!_.isEmpty(skipJobs)) {
            log.info('BUILD SKIP LIST: ' + _.pluck(skipJobs, 'name').toString());
        }
        return callback(null, filteredJobs);
    } else {
        return callback('Need dependency packages: [' + needPkgs.toString() + ']');
    }
}

function setWorkspace(cwd, jobs, callback) {
    async.eachSeries(jobs, function (job, cb) {
        createWorkspace(cwd, job, cb);
    }, function (err) {
        callback(err, jobs);
    });
}

function createWorkspace(cwd, job, callback) {
    job.buildRoot = path.join(cwd, '.ts-cli', 'full-build', job.name);

    async.waterfall([
        function (cb) {
            fs.access(job.buildRoot, function (err) {
                if (err) {
                    return cb(null, false);
                } else {
                    return cb(null, true);
                }
            });
        }, function (exist, cb) {
            if (exist) {
                extfs.remove(job.buildRoot, function (err) {
                    cb(err);
                });
            } else {
                return cb(null);
            }
        }, function (cb) {
            extfs.mkdirs(job.buildRoot, function () {
                cb(null);
            });
        }
    ], function (err) {
        if (err) {
            return callback(err);
        }

        job.logPath = path.join(job.buildRoot, 'log');
        job.log = new (winston.Logger)({
            transports: [
                new (winston.transports.File)({
                    filename: job.logPath,
                    timestamp: function () {
                        var now = new Date();
                        return now.format('YYYY-MM-DD hh:mm:ss.SS');
                    },
                    handleExceptions: true,
                    json: false
                })
            ],
            exceptionHandlers: [new winston.transports.File({
                filename: 'logException.log'
            })],
            exitOnError: false
        });

        job.log.info('Create Job log - ' + job.name);
        callback(null, job);
    });
}

function createJob(cwd, files, log, options, callback) {
    var resourcesDir = options.resources || path.join(cwd, 'resource');
    resourcesDir = path.resolve(resourcesDir);

    async.map(files, function (file, cb) {
        var filePath = path.join(cwd, file);
        fs.lstat(filePath, function (err, stat) {
            if (err) {
                return cb(err);
            } else if (stat.isDirectory()) {
                if (options.exclude.indexOf(file) !== -1) {
                    // skip
                    return cb(null);
                } else if (filePath === resourcesDir) {
                    createResourceJob(filePath, log, cb);
                } else {
                    fs.access(path.join(filePath, 'package/pkginfo.manifest'), function (err) {
                        if (err) {
                            return cb(null);
                        } else {
                            TsJob.create(filePath, {
                                log: log
                            }, cb);
                        }
                    });
                }
            } else {
                return cb(null);
            }
        });
    }, function (err, jobs) {
        if (err) {
            callback(err);
        } else {
            jobs = _.compact(jobs);
            jobs = _.flatten(jobs, true);
            callback(null, jobs);
        }
    });
}

function createResourceJob(dir, log, callback) {
    async.waterfall([
        function (cb) {
            fs.readdir(dir, cb);
        },
        function (files, cb) {
            async.map(files, function (file, cb1) {
                var extName = path.extname(file);
                var filePath = path.join(dir, file);
                if (extName === '.zip' || extName === '.gz' || extName === '.bz2') {
                    //TsJob.create(filePath, {log: log}, cb1);
                    TsJob.create(filePath, {
                        log: log
                    }, function (err, job) {
                        cb1(err, job);
                    });
                } else {
                    cb1(null);
                }
            }, cb);
        }
    ], function (err, jobs) {
        if (err) {
            callback(err);
        } else {
            jobs = _.compact(jobs);
            callback(null, jobs);
        }
    });
}
