/**
 * builder.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
**/

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

var utils = require('../../lib/utils.js');
var Tutils = require('../org.tizen.common/tizen_utils');
var Package = require('../org.tizen.common/package');
var Process = require('../dibs.core/process.js');
var Zip = require('../dibs.core/zip.js');
var FileSystem = require('../dibs.core/filesystem.js');


/**
* DIBS tizen source build module
* @module models/tizen-project/builder
*/

/**
 * get package information from pkginfo.manifest file
 * @private
 * @function getPkgInfo
 * @param {string} pkgInfoPath - Absolute path of pkginfo.manifest file
 * @param {module:mode../org.tizen.common/tizenUtils.host} host - host infomation HASh{os,category}
 * @param {module:models/job.options} options - job options
 * @param {module:mode../org.tizen.common/parser.callback_err_pkginfos} callback - callback (error, pkginfos)
 * @memberOf module:models/tizen-project/builder
 */

function getPkgInfo(pkgInfoPath, host, options, callback) {
    fs.exists(pkgInfoPath, function (exist) {
        if (!exist) {
            var error = new Error(pkgInfoPath + ' is not exist!');
            callback(error, null);
        } else {
            async.waterfall([
                function (wcb) {
                    Package.getPkgListFromFile(pkgInfoPath, wcb);
                },
                function (pkgList, wcb) {
                    //get available package in this host evnironment
                    //
                    var availablePkgList = _.select(pkgList, function (pkg) {
                        return (pkg.buildHostOS === host.os &&
                            pkg.osList.indexOf(options.TARGET_OS) !== -1);
                    });

                    if (availablePkgList.length === 0) {
                        wcb(new Error('' + host.os + ' server can\'t build any of packages in (' + _.uniq(_.pluck(pkgList, 'name')) + ')'));
                    } else {
                        wcb(null, availablePkgList);
                    }
                }
            ], callback);
        }
    });
}

/**
 * Generate script and Run using package information and host os infomation
 * @private
 * @function runScript
 * @param {string} workDir - Absolute path of workspace
 * @param {string} buildRoot - Absolute path of buildRoot
 * @param {Array.<module:mode../org.tizen.common/parser.pkginfo>} pkglist - list of package information
 * @param {module:models/job.options} options - job options
 * @param {module:mode../org.tizen.common/tizenUtils.host} host - host infomation HASH{os,category}
 * @param {string} target - script function name (clean/build/install)
 * @param {object} monitor - monitor object
 * @param {module:lib/utils.callback_error} callback - callback (error)
 * @memberOf module:models/tizen-project/builder
 */

function runScript(workdir, buildRoot, buildScript, pkglist, options, host, target, monitor, callback) {
    var runFile = path.join(workdir, '.build.' + utils.generateTimeStamp(true) + '.sh');
    async.waterfall([
        //gen script
        function (wcb) {
            monitor.updateProgress(' - reading script file...', wcb);
        },
        function (wcb) {
            fs.readFile(buildScript, wcb);
        },
        function (scriptBuffer, wcb) {
            var option = ' ';
            switch (target) {
            case 'build_cache':
                option = 'CACHEDIR=${PKG_CACHE_DIR}/$(cache_key)';
                break;
            case 'save_cache':
                option = 'rm -rf ${PKG_CACHE_DIR}/*\nCACHEDIR=${PKG_CACHE_DIR}/$(cache_key)\nmkdir -p ${CACHEDIR}';
                break;
            default:
                break;
            }
            option = option + '\n' + target + '\necho "success"';
            // remove tail and add options
            wcb(null, '#!/bin/bash -xe\n' + scriptBuffer.toString().replace(/[^}]*$/, '') + '\n' + option);
        },
        // write script
        function (scriptString, wcb) {
            wcb(fs.writeFileSync(runFile, scriptString, {
                mode: 493
            }));
        },
        function (wcb) {
            monitor.updateProgress(' - executing ' + path.basename(runFile) + ' ...', wcb);
        },
        // run
        function (wcb) {
            var env = _.clone(process.env);
            env.BUILD_TARGET_OS = options.TARGET_OS;
            env.TARGET_OS = options.TARGET_OS;
            env.TARGET_OS_CATEGORY = Tutils.getOSCategory(options.TARGET_OS);
            env.SRCDIR = workdir;
            env.ROOTDIR = buildRoot;
            env.VERSION = pkglist[0].version;

            if (options.DISTRIBUTION) {
                env.DISTRIBUTION = options.DISTRIBUTION;
            } else {
                env.DISTRIBUTION = '';
            }

            var cmd;
            var pathDelimiter;
            if (os.platform() === 'win32') {
                pathDelimiter = ';';
                cmd = 'cmd';
                // Added env variable for msys support
                env.MSYS_ROOTDIR = utils.convertToMsysPath(buildRoot);
                env.MSYS_SRCDIR = utils.convertToMsysPath(workdir);
            } else {
                cmd = 'bash';
                pathDelimiter = ':';
            }

            if (options.TOOLCHAIN && options.TOOLCHAIN.TOOLCHAIN_PATH) {
                monitor.updateProgress(' - running script with toolchain');
                env.Path = process.env.PATH;
                if (options.TOOLCHAIN.BIN_PATH) {
                    _.each(options.TOOLCHAIN.BIN_PATH, function (p) {
                        var bPath = p.split('/').join(path.sep);
                        bPath = path.join(options.TOOLCHAIN.TOOLCHAIN_PATH, bPath);
                        env.Path = bPath + pathDelimiter + env.Path;
                    });
                    monitor.updateProgress('  - PATH: ' + env.Path.toString());
                }
                var cPath = options.TOOLCHAIN.CMD_PATH.split('/').join(path.sep);
                cmd = path.join(options.TOOLCHAIN.TOOLCHAIN_PATH, cPath);
                monitor.updateProgress('  - CMD: ' + cmd.toString());
            }
            monitor.updateProgress(' - executing ' + path.basename(runFile) + ' ...');

            var script = Process.create(cmd,
                ['-c', utils.path2string(runFile)],
                {
                    cwd: workdir,
                    env: env
                },
                {
                    onStdout: function (line) {
                        monitor.updateProgress('   ' + line);
                    },
                    onStderr: function (line) {
                        monitor.updateProgress('   ' + line);
                    },
                    onExit: function (code) {
                        if (code !== 0) {
                            var error;
                            if (monitor.isCancel()) {
                                error = new Error('cancel');
                                monitor.updateProgress({
                                    log: ' - script failed by cancel',
                                    logType: 'error'
                                });
                            } else {
                                error = new Error('Running ' + target + ' script failed with exit code ' + code);
                                monitor.updateProgress({
                                    log: ' - script failed with code : ' + code,
                                    logType: 'error'
                                });
                            }
                            wcb(error);
                        } else {
                            monitor.updateProgress(' - running ' + target + ' script succeeded', wcb);
                        }
                    }
                });
            monitor.addProcess(script);
        }
    ], function (err) {
        //utils.removePathIfExist(runFile, function () {
            callback(err);
        //});
    });
}

function runGradleScript(workdir, buildRoot, buildScript, pkglist, options, host, monitor, callback) {
    var env = _.clone(process.env);
    var cmd;
    var pathDelimiter;
    var args = [];

    if (os.platform() === 'win32') {
        pathDelimiter = ';';
        args.push('gradlew');
        env.MSYS_ROOTDIR = utils.convertToMsysPath(buildRoot);
        args.push('-DMSYS_ROOTDIR=' + env.MSYS_ROOTDIR);
        env.MSYS_SRCDIR = utils.convertToMsysPath(workdir);
        args.push('-DMSYS_SRCDIR=' + env.MSYS_SRCDIR);
    } else {
        cmd = './gradlew';
        pathDelimiter = ':';
    }

    args.push('--info');
    args.push('-DBUILD_TARGET_OS=' + options.TARGET_OS);
    args.push('-DTARGET_OS=' + options.TARGET_OS);
    args.push('-DTARGET_OS_CATEGORY=' + Tutils.getOSCategory(options.TARGET_OS));
    args.push('-DSRCDIR=' + workdir);
    args.push('-DROOTDIR=' + buildRoot);
    args.push('-DVERSION=' + pkglist[0].version);

    if (options.DISTRIBUTION) {
        args.push('-DDISTRIBUTION=' + options.DISTRIBUTION);
    } else {
        args.push('-DDISTRIBUTION=' + ' ');
    }

    if (options.TOOLCHAIN && options.TOOLCHAIN.TOOLCHAIN_PATH) {
        monitor.updateProgress(' - running script with toolchain');
        env.Path = process.env.PATH;
        if (options.TOOLCHAIN.BIN_PATH) {
            _.each(options.TOOLCHAIN.BIN_PATH, function (p) {
                var bPath = p.split('/').join(path.sep);
                bPath = path.join(options.TOOLCHAIN.TOOLCHAIN_PATH, bPath);
                env.Path = bPath + pathDelimiter + env.Path;
            });
        }
        monitor.updateProgress('  - PATH: ' + env.Path.toString());
        var cPath = options.TOOLCHAIN.CMD_PATH.split('/').join(path.sep);
        cmd = path.join(options.TOOLCHAIN.TOOLCHAIN_PATH, cPath);
    }
    monitor.updateProgress(' - executing gradlew ...');
    monitor.updateProgress('  - CMD: ' + cmd.toString());
    monitor.updateProgress('  - ARGS: ' + args.toString());

    var script = Process.create(cmd, args,
        {
            cwd: path.join(workdir, 'package'),
            env: env
        },
        {
            onStdout: function (line) {
                monitor.updateProgress('   ' + line);
            },
            onStderr: function (line) {
                monitor.updateProgress('   ' + line);
            },
            onExit: function (code) {
                if (code !== 0) {
                    var error;
                    if (monitor.isCancel()) {
                        error = new Error('cancel');
                        monitor.updateProgress({
                            log: ' - script failed by cancel',
                            logType: 'error'
                        });
                    } else {
                        error = new Error('Running gradle script failed with exit code ' + code);
                        monitor.updateProgress({
                            log: ' - script failed with code : ' + code,
                            logType: 'error'
                        });
                    }
                    callback(error);
                } else {
                    monitor.updateProgress(' - running gradle script succeeded', callback);
                }
            }
        });
    monitor.addProcess(script);
}

function installScriptFiles(scriptType, workDir, installDir, pkg, monitor, callback) {

    async.waterfall([
        function (cb) {
            monitor.updateProgress(' - finding ' + scriptType + ' script(s)...', cb);
        },
        function (cb) {
            findScriptFiles(scriptType, pkg.name, pkg.osList, workDir, monitor, cb);
        },
        function (scripts, cb) {
            if (scripts.length > 0) {
                monitor.updateProgress(' - copying ' + scriptType + ' script(s)...');
                copyScriptFiles(scripts, installDir, cb);
            } else {
                monitor.updateProgress(' - no ' + scriptType + ' script exist', cb);
            }
        }], function (err) {
        callback(err);
    });
}


function findScriptFiles(scriptType, pkgName, osList, workDir, monitor, callback) {
    var scripts = [];

    async.eachSeries(osList,
        function (osName, cb) {
            findScriptFileForTargetOS(scriptType, pkgName, osName, workDir, monitor, function (err, script) {
                if (script && scripts.indexOf(script) === -1) {
                    scripts.push(script);
                }
                cb(null);
            });
        }, function (err) {
            callback(null, scripts);
        });
}


function findScriptFileForTargetOS(scriptType, pkgName, osName, workDir, monitor, callback) {
    var candidates = [];

    // check os name
    var ext = Tutils.getOSCategory(osName) === 'windows' ? 'BAT' : 'sh';
    candidates.push(path.join(workDir, 'package',
        pkgName + '.' + scriptType + '.' + osName + '.' + ext));
    candidates.push(path.join(workDir, 'package',
        pkgName + '.' + scriptType + '.' + osName));

    // check osCategory
    var osCategory = Tutils.getOSCategory(osName);
    candidates.push(path.join(workDir, 'package',
        pkgName + '.' + scriptType + '.' + osCategory + '.' + ext));
    candidates.push(path.join(workDir, 'package',
        pkgName + '.' + scriptType + '.' + osCategory));

    // get existing files
    async.detectSeries(candidates, fs.exists,
        function (script) {
            callback(null, script);
        });
}


function copyScriptFiles(scripts, installDir, callback) {
    var simple = (scripts.length === 1);
    async.eachSeries(scripts,
        function (scriptPath, cb) {
            var baseName = path.basename(scriptPath);
            // if string "abcde.remove.windows-32.BAT" then
            // strFormat as ['.remove.windows-32.BAT','remove','windows-32','.BAT',index:5,input:'abcde.remove.windows-32.BAT']
            var strFormat = baseName.match(/.(install|remove).([^.]+)(.BAT|.sh)?$/);
            var destScriptPath = null;
            if (strFormat) {
                var scriptType = strFormat[1];
                var scriptOS = strFormat[2];
                var scriptExt = strFormat[3];
                if (!scriptExt) {
                    scriptExt = (Tutils.getOSCategory(scriptOS) === 'windows' || scriptOS === 'windows') ? '.BAT' : '.sh';
                }
                if (simple) {
                    destScriptPath = path.join(installDir, scriptType + scriptExt);
                } else {
                    destScriptPath = path.join(installDir, scriptType + scriptOS + scriptExt);
                }
            } else {
                cb(new Error('Invalid script name! : ' + baseName));
            }

            async.waterfall([
                function (cb1) {
                    extfs.copy(scriptPath, destScriptPath, cb1);
                },
                function (cb1) {
                    fs.chmod(destScriptPath, '0755', cb1);
                }
            ], cb);
        }, function (err) {
            callback(err);
        });
}

function packaging(workdir, buildRoot, pkglist, changelog, options, host, monitor, callback) {
    monitor.updateProgress('Creating result package file(s)...');
    async.eachSeries(pkglist, function (pkg, ecb) {
        var installDir = '';
        async.waterfall([
            // get install directory path
            function (wcb) {
                var installPath = path.join(workdir, 'package',
                    pkg.name + '.package.' + options.TARGET_OS);
                fs.exists(installPath, function (isExist) {
                    if (isExist) {
                        installDir = installPath;
                        wcb(null);
                    } else {
                        var error = new Error(pkg.name + '.package.' + options.TARGET_OS + ' must be created!! It caused wrong \'pkginfo.manifest\' or \'build\' script');
                        wcb(error);
                    }
                });
            },
            function (wcb) {
                monitor.updateProgress('Calculating uncompress size...' + pkg.name, wcb);
            },
            // get Uncompressed-size
            function (wcb) {
                utils.getDirSizeRecursive(installDir, wcb);
            },
            // copy install script
            function (size, wcb) {
                pkg.uncompSize = size;
                monitor.updateProgress('Copying install script...', wcb);
            },
            function (wcb) {
                installScriptFiles('install', workdir, installDir, pkg, monitor, wcb);
            },
            function (wcb) {
                monitor.updateProgress('Copying remove script...', wcb);
            },
            // copy remove script
            function (wcb) {
                installScriptFiles('remove', workdir, installDir, pkg, monitor, wcb);
            },
            function (wcb) {
                monitor.updateProgress('Generating pkginfo.manifest...', wcb);
            },
            // gen pkginfo.manifest file in installDir
            function (wcb) {
                fs.writeFile(path.join(installDir, 'pkginfo.manifest'),
                    Package.pkgString(pkg), {
                        encoding: 'utf-8'
                    }, wcb);
            },
            function (wcb) {
                monitor.updateProgress('Copying changelog file...', wcb);
            },
            // copy changelog file in installDir
            function (wcb) {
                if (changelog) {
                    fs.writeFile(path.join(installDir, 'changelog'),
                        changelog, {
                            encoding: 'utf-8'
                        }, function (err) {
                            if (err) {
                                wcb(err);
                            } else {
                                monitor.updateProgress(' - copying changelog done', wcb);
                            }
                        });
                } else {
                    wcb(null);
                }
            },
            function (wcb) {
                var zipFile = pkg.name + '_' + pkg.version + '_' + options.TARGET_OS + '.zip';
                monitor.updateProgress('Creating package file ... ' + zipFile, function (err) {
                    wcb(err, zipFile);
                });
            },
            // zipping
            function (zipFile, wcb) {
                var targetFile = path.join(workdir, zipFile);
                var zip = Zip.compress(targetFile, installDir,
                    {
                        onStdout: function (line) {
                            monitor.updateProgress(line);
                        },
                        onStderr: function (line) {
                            monitor.updateProgress({
                                log: line,
                                logType: 'error'
                            });
                        },
                        onExit: function (code) {
                            if (code !== 0) {
                                var error = new Error('zip process exited with code ' + code);
                                wcb(error);
                            } else {
                                monitor.updateProgress(' - ' + targetFile + ' created', wcb);
                            }
                        }
                    });
                monitor.addProcess(zip);
            }
        ],
            function (error) {
                if (error) {
                    monitor.updateProgress({
                        log: error.message,
                        logType: 'error'
                    });
                }
                ecb(error);
            });
    }, function (err) {
        var error = null;
        if (err) {
            error = new Error('Packaging failed!');
        }
        callback(error);
    });
}

function cleanPkgDir(workDir, pkgList, host, callback) {
    // remove package directory
    var pkgDirList = _.flatten(_.map(pkgList, function (pkg) {
        return _.map(_.values(host), function (ext) {
            return path.join(workDir, 'package', pkg.name + '.package.' + ext);
        });
    }));
    async.select(pkgDirList, fs.exists, function (results) {
        async.each(results, FileSystem.remove, callback);
    });
}

function executeShellBuildScript(srcPath, buildRoot, buildScript, pkgList, options, host, monitor, callback) {
    async.waterfall([
        function (cb) {
            monitor.updateProgress('Running script: clean...');
            runScript(srcPath, buildRoot, buildScript, pkgList, options, host, 'clean', monitor, cb);
        }, function (cb) {
            monitor.updateProgress('Running script: build...');
            runScript(srcPath, buildRoot, buildScript, pkgList, options, host, 'build', monitor, cb);
        }, function (cb) {
            monitor.updateProgress('Running script: install...');
            runScript(srcPath, buildRoot, buildScript, pkgList, options, host, 'install', monitor, cb);
        }
    ], callback);
}

function executeGradleBuildScript(srcPath, buildRoot, buildScript, pkgList, options, host, monitor, callback) {
    monitor.updateProgress('Running gradle script...');
    runGradleScript(srcPath, buildRoot, buildScript, pkgList, options, host, monitor, callback);
}

/**
 * Build and packaging tizen git Job
 * @function build
 * @param {string} srcPath - Absolute path of pkginfo.manifest file
 * @param {module:mode../org.tizen.common/tizenUtils.host} host - host infomation {os,category}
 * @param {module:models/job.options} options - job options HASH{key,value}
 * @param {module:lib/utils.callback_error} callback - callback(error)
 * @memberOf module:models/tizen-project/builder
 */

function build(srcPath, buildRoot, host, options, monitor, callback) {
    var pkgList = [];
    var buildScript = '';
    var manifestPath = path.join(srcPath, 'package', 'pkginfo.manifest');
    var changelogPath = path.join(srcPath, 'package', 'changelog');
    async.waterfall([
        function (cb) {
            monitor.updateProgress('Parsing package/pkginfo.manifest file...');
            getPkgInfo(manifestPath, host, options, cb);
        }, function (pkgListInfo, cb) {
            pkgList = pkgListInfo;

            monitor.updateProgress('Checking build script...');
            // build script priority
            var exts = [];
            exts.push('gradle');        // 1. build.gradle
            exts.push(host.os);         // 2. build.${host.os}
            exts.push(host.category);   // 3. build.${host.category}

            var buildScriptList = _.map(exts, function (ext) {
                return path.join(srcPath, 'package', 'build.' + ext);
            });
            // Result will be the first item in the array that passes the truth test.
            async.detectSeries(buildScriptList, fs.exists, function (result) {
                if (result) {
                    cb(null, result);
                } else {
                    var error = new Error('Build script must be exists!: build.' + host.os + '|build.' + host.category + '|build.gradle');
                    cb(error, null);
                }
            });
        }, function (buildScriptInfo, cb) {
            buildScript = buildScriptInfo;
            monitor.updateProgress('Preparing build directory...');
            cleanPkgDir(srcPath, pkgList, host, cb);
        }, function (cb) {
            if (path.basename(buildScript) === 'build.gradle') {
                executeGradleBuildScript(srcPath, buildRoot, buildScript, pkgList, options, host, monitor, cb);
            } else {
                executeShellBuildScript(srcPath, buildRoot, buildScript, pkgList, options, host, monitor, cb);
            }
        }, function (cb) {
            monitor.updateProgress('Generating build result packages...');
            fs.exists(changelogPath, function (exist) {
                if (exist) {
                    fs.readFile(changelogPath, {
                        encoding: 'utf-8'
                    }, cb);
                } else {
                    cb(null, null);
                }
            });
        }, function (changelog, cb) {
            monitor.updateProgress('Packaging...');
            packaging(srcPath, buildRoot, pkgList, changelog, options, host, monitor, cb);
        }
    ], callback);
}

module.exports.build = build;
