/**
 * job-schedule.js
 * Copyright (c) 2000 - 2015 Samsung Electronics Co., Ltd. All rights reserved.
 *
 * Contact:
 * DongHee Yang <donghee.yang@samsung.com>
 * Sungmin Kim <sm.art.kim@samsung.com>
 * Jiil Hyoun <jiil.hyoun@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
**/

var dibs = require('../../core/dibs');
var DError = require('../../core/exception');
var _ = require('underscore');
var async = require('async');


module.exports.scheduleToInitializeJobs = function (jobs, callback) {
    var servers = dibs.getAllServers();

    var result = {
        assignedJobs: {},
        errorJobs: {},
        unassignedJobs: {}
    };

    // get working jobs of current build servers
    var workingJobCount = getCurrentWorkingJobCount(jobs);

    async.eachSeries(
        _.select(jobs, function (job) {
            return (job.status === 'JUST_CREATED');
        }),
        function (job, cb) {
            async.waterfall([
                function (cb1) {
                    var availableServers = _.select(servers, function (svr) {
                        if (svr.type !== 'builder') {
                            return false;
                        }
                        if (svr.status !== 'RUNNING') {
                            return false;
                        }
                        if (job.assignedBuilderId && (job.assignedBuilderId !== svr.id)) {
                            return false;
                        }
                        if (job.initEnvironments &&
                            _.intersection(job.initEnvironments, svr.environments.map(
                                function (e) {
                                    return e.id;
                                })).length === 0) {
                            return false;
                        }
                        return true;
                    });
                    // check server availability
                    if (availableServers.length < 1) {
                        cb1(new DError('JOBMGR006', {
                            jobId: job.id
                        }), null);
                    } else {
                        cb1(null, availableServers);
                    }
                },
                function (availableServers, cb) {
                    getRecommendedServer(availableServers, workingJobCount, cb);
                }],
                function (err, builder) {
                    if (err) {
                        result.errorJobs[job.id] = err;
                    } else {
                        if (builder) {
                            result.assignedJobs[job.id] = builder;
                            if (!workingJobCount[builder.id]) {
                                workingJobCount[builder.id] = 1;
                            } else {
                                workingJobCount[builder.id]++;
                            }
                        }
                    }
                    cb(null);
                });
        },
        function (err) {
            callback(err, result);
        });
};


module.exports.scheduleToBuildJobs = function (jobs, conflictJobs, options, callback) {
    var servers = dibs.getAllServers();

    var result = {
        assignedJobs: {},
        errorJobs: {},
        unassignedJobs: {}
    };
    // get working jobs of current build servers
    var workingJobCount = getCurrentWorkingJobCount(jobs);

    var candidateJobs = _.select(jobs,
        function (job) {
            // check my status
            if (job.status !== 'INITIALIZED') {
                return false;
            }

            // if parent job is not working, cannot be selected
            if (job.parentId && jobs[job.parentId] && jobs[job.parentId].status !== 'WORKING') {
                return false;
            }

            // if dependent job is not finished, cannot be selected
            for (var i = 0; i < job.depJobs.length; i++) {
                if (jobs[job.depJobs[i]].status !== 'FINISHED') {
                    return false;
                }
            }

            // check mutually exclusive job
            if (!job.parentId) {
                var projectType = dibs.projectTypes[job.projectType];
                for (var jobId in conflictJobs) {
                    // check if it exists in conflict list
                    var filtered = conflictJobs[jobId].filter(function (e) {
                        return e === job.id;
                    });
                    if (filtered.length > 0) {
                        return false;
                    }

                    // check only if this job is not reverse job, test build
                    if (!(!job.options.UPLOAD && job.options.BUILD_ONLY) &&
                        !job.options.UPLOAD_TEMP &&
                        projectType.module.checkMutualExclusiveJob &&
                        projectType.module.checkMutualExclusiveJob(job, jobs[jobId])) {
                        conflictJobs[ jobId ].push(job.id);
                        return false;
                    }
                }
            }

            if (!existsAvailableServers(job, servers, workingJobCount)) {
                return false;
            }

            return true;
        });

    if (candidateJobs.length > 0) {
        chooseBestJobInCandidates(candidateJobs, servers, workingJobCount, options, callback);
    } else {
        callback(null, result);
    }
};


function existsAvailableServers(job, servers, workingJobCount) {
    var targetServers = _.select(servers, function (svr) {
        if (svr.type !== 'builder') {
            return false;
        }
        if (svr.status !== 'RUNNING') {
            return false;
        }
        if (job.execEnvironments &&
                _.intersection(job.execEnvironments, svr.environments.map(function (e) {
                    return e.id;
                })).length === 0) {
            return false;
        }
        if (workingJobCount[svr.id] && workingJobCount[svr.id] >= svr.config.get('max_jobs')) {
            return false;
        }

        return true;
    });

    return targetServers.length > 0;
}


function getCurrentWorkingJobCount(jobs) {
    var result = {};

    for (var jobId in jobs) {
        var job = jobs[jobId];
        var serverId = null;
        if (job.status === 'WORKING') {
            serverId = job.execServerId;
        }
        if (job.status === 'INITIALIZING') {
            serverId = job.initServerId;
        }

        if (serverId) {
            if (!result[serverId]) {
                result[serverId] = 1;
            } else {
                result[serverId]++;
            }
        }
    }

    return result;
}


function getRecommendedServer(builders, workingJobCount, callback) {

    // select server with no working jobs
    var result = selectServerWithMinimumWorkingJobs(builders, workingJobCount);

    if (result.length === 0) {
        callback(null, null);
    } else if (result.length === 1) {
        callback(null, result[0]);
    } else {
        // select serer with minimum cpu usage
        selectServerWithMinCpuUsage(result, callback);
    }
}


function selectServerWithMinimumWorkingJobs(builders, workingJobCount) {
    var result = builders.filter(function (e) {
        return !workingJobCount[e.id] || workingJobCount[e.id] === 0;
    });
    if (result.length > 0) {
        return result;
    }

    var min = 9999;
    for (var i = 0; i < builders.length; i++) {
        var builder = builders[i];
        var val = workingJobCount[builder.id] / builder.config.get('max_jobs');

        // skip if alreay reach max working jobs
        if (val >= 1) {
            continue;
        }

        if (val < min) {
            result = [];
            result.push(builder);
            min = val;
        } else if (val === min) {
            result.push(builder);
        }
    }

    return result;
}


function selectServerWithMinCpuUsage(builders, callback) {
    var builderIds = builders.map(function (builder) {
        return builder.id;
    });

    async.map(builderIds,
        function (builderId, cb) {
            dibs.rpc.master.getServerStatus(builderId, function (err, status) {
                if (err) {
                    cb(null, {
                        id: builderId,
                        status: 'DISCONNECTED',
                        cpuUsage: 0,
                        memUsage: 0,
                        diskUsage: 0
                    });
                } else {
                    status.id = builderId;
                    cb(null, status);
                }
            });
        },
        function (err, statList) {
            if (err) {
                callback(err, null);
            } else {
                var runningStatus = _.where(statList, {status: 'RUNNING'});
                var cpuList = _.map(runningStatus, function (stat) {
                    return stat.cpuUsage;
                });
                var minCpu = _.min(cpuList);
                var index = _.indexOf(cpuList, minCpu);
                callback(err, builders[index]);
            }
        });
}


function chooseBestJobInCandidates(candidateJobs, servers, workingJobCount, options, callback) {
    var pairs = [];
    async.series([
        function (cb) {
            if (options.scheduleUsingExecTime) {
                // get project infos of candidate jobs
                async.each(candidateJobs,
                    function (job, cb1) {
                        dibs.rpc.datamgr.searchProjects({
                            name: job.projectName,
                            distName: job.distName
                        }, function (err, prjs) {
                            if (err) {
                                cb1(err);
                            } else {
                                pairs.push({
                                    job: job,
                                    project: prjs[0]
                                });
                                cb1(null);
                            }
                        });
                    },
                    function () {
                        cb(null);
                    });
            } else {
                cb(null);
            }
        }],
        function () {
            var sorted = candidateJobs;
            if (options.scheduleUsingExecTime) {
                // sort job by avg.exec.time
                sorted = candidateJobs.sort(function (j1, j2) {
                    var p1 = pairs.filter(function (e) { return e.job === j1; })[0].project;
                    var p2 = pairs.filter(function (e) { return e.job === j2; })[0].project;

                    if (p1 && p2 && p1.options['AVG_SUCC_EXEC_TIME'] > 0 &&
                        p2.options['AVG_SUCC_EXEC_TIME']) {

                        return p2.options['AVG_SUCC_EXEC_TIME'] - p1.options['AVG_SUCC_EXEC_TIME'];
                    } else if (p1 && p1.options['AVG_SUCC_EXEC_TIME'] > 0) {
                        return -1;
                    } else if (p2 && p2.options['AVG_SUCC_EXEC_TIME'] > 0) {
                        return 1;
                    } else {
                        return j1.id - j2.id;
                    }
                });
            }

            // return first available job
            async.map(sorted,
                function (job, cb1) {
                    chooseBuilderForJob(job, servers, workingJobCount, cb1);
                }, function (err, results) {
                    for(var i = 0; i < results.length; i++) {
                        if (Object.keys(results[i].assignedJobs).length > 0) {
                            callback(null, results[i]);
                            return;
                        }
                    }
                    callback(null, results[0]);
                });
        });
}


function chooseBuilderForJob(selectedJob, servers, workingJobCount, callback) {
    var result = {
        assignedJobs: {},
        errorJobs: {},
        unassignedJobs: {}
    };

    async.waterfall([
        function (wcb) {
            var availableServers = _.select(servers, function (svr) {
                if (svr.type !== 'builder') {
                    return false;
                }
                if (svr.status !== 'RUNNING') {
                    return false;
                }
                if (selectedJob.assignedBuilderId && (selectedJob.assignedBuilderId !== svr.id)) {
                    return false;
                }
                if (selectedJob.execEnvironments &&
                    _.intersection(selectedJob.execEnvironments, svr.environments.map(
                        function (e) {
                            return e.id;
                        })).length === 0) {
                    return false;
                }
                return true;
            });

            if (availableServers.length < 1) {
                wcb(new DError('JOBMGR007', {
                    jobId: selectedJob.id
                }), null);
            } else {
                wcb(null, availableServers);
            }
        },
        function (availableServers, cb) {
            getRecommendedServer(availableServers, workingJobCount, cb);
        }
    ],
    function (err, builder) {
        if (err) {
            result.errorJobs[selectedJob.id] = err;
        } else {
            if (builder) {
                result.assignedJobs[selectedJob.id] = builder;
            }
        }
        callback(null, result);
    });
}
