/**
 * installer-generator.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 path = require('path');
var async = require('async');
var _ = require('underscore');
var fs = require('fs');
var extfs = require('fs-extra');
var os = require('os');

var dibs = require('../../core/dibs.js');
var utils = require('../../lib/utils.js');
var FileSystem = require('../dibs.core/filesystem.js');
var Process = require('../dibs.core/process.js');
var Zip = require('../dibs.core/zip.js');
var RemoteRepo = require('../org.tizen.repository/remote-repo.js');
var Installer = require('../org.tizen.projects/installer.js');
var Tutils = require('../org.tizen.common/tizen_utils.js');
var DError = require('../../core/exception.js');
var Package = require('../org.tizen.common/package.js');
var ProgressMonitor = require('../../lib/monitor.js');

module.exports.generate = makeInstaller;

var INST_GEN_SCRIPT = 'buildInstaller';


function makeInstaller(serverURL, distName, targetOS, opts, monitor, callback) {
    var installDir = null;
    var repo = null;
    var snapshot = null;

    var installerInfo = null;

    // TODO: Need to verify target OS
    async.waterfall([
        function (cb) {
            monitor.updateProgress('Initializing workspace...');
            initializeWorkspace(targetOS, opts, monitor, cb);
        },
        function (dir, cb) {
            installDir = dir;

            monitor.updateProgress('Getting target snapshot...');
            loadTargetSnapshot(serverURL, distName, opts, monitor, cb);
        },
        function (r, s, cb) {
            repo = r;
            snapshot = s;
            monitor.updateProgress('Generating SDK installer image...');
            setTimeout(function () {
                generateInstallerImage(repo, distName, targetOS, snapshot,
                    installDir, opts, monitor, function (err, results) {
                        if (results) {
                            installerInfo = results;
                        }
                        cb(err);
                    });
            }, 2000);
        },
        function (cb) {
            monitor.updateProgress('Creating SDK installer...');
            createInstaller(repo, distName, targetOS, snapshot, opts, monitor, cb);
        }
    ],
    function (err) {
        callback(err, installerInfo);
    });
}


function initializeWorkspace(targetOS, opts, monitor, callback) {

    // check workspace directory
    var workspace = process.cwd();
    if (opts.workspace) {
        workspace = opts.workspace;
    }

    var installDir = path.join(workspace, targetOS);

    async.waterfall([
        // check workspace directory
        function (cb) {
            fs.exists(workspace, function (exists) {
                if (!exists) {
                    extfs.mkdirp(workspace, cb);
                } else {
                    cb(null);
                }
            });
        },
        function (cb) {
            utils.removePathIfExist(installDir, cb);
        },
        function (cb) {
            monitor.updateProgress('Creating temporary install directory... ' + installDir);
            extfs.mkdirp(installDir, cb);
        }
    ],
    function (err, installDir) {
        callback(err, installDir);
    });
}


function loadTargetSnapshot(serverURL, distName, opts, monitor, callback) {

    var repo = null;

    // check snapshot name
    var snapshotName = null;
    if (opts.snapshotName) {
        snapshotName = opts.snapshotName;
    }

    async.waterfall([
        // get repository control
        function (cb) {
            if (serverURL) {
                repo = RemoteRepo.createRemoteRepo(serverURL, {
                    distName: distName,
                    snapshotName: snapshotName
                });
                repo.open(null, cb);
            } else {
                repo = dibs.getServersByType('repo')[0];
                cb(null);
            }
        },
        function (cb) {
            repo.searchSnapshots({
                name: snapshotName,
                repoType: 'tizen',
                distName: distName
            }, function (err, snapshots) {
                if (!err) {
                    if (snapshots.length === 1) {
                        cb(err, repo, snapshots[0]);
                    } else {
                        cb(new DError('TIZENDEPLOY002', {
                            sname: snapshotName
                        }), null, null);
                    }
                } else {
                    cb(err, null, null);
                }
            });
        }
    ],
    function (err, repo, snapshot) {
        callback(err, repo, snapshot);
    });
}


function getDependentPackages(pkgList, pkgOS, remoteSnapshot, depPkgList, monitor) {
    var depPackageList = [];
    if (depPkgList) {
        depPackageList = _.clone(depPkgList);
    }

    _.each(pkgList, function (pkg) {
        var pkgInfo = remoteSnapshot.osPackages[pkg.os][pkg.name];

        if (pkgInfo.attr === 'root' && (pkg.parentPkg && pkg.parentPkg.attr === 'root')) {
            monitor.updateProgress('skip ' + pkgInfo.name + ' because of meta dependency');
        } else {
            monitor.updateProgress('add ' + pkgInfo.name + ' into depPackageList');

            depPackageList.push({
                name: pkgInfo.name,
                version: pkgInfo.version,
                os: pkgInfo.os,
                size: pkgInfo.size,
                attr: pkgInfo.attr === 'root' ? pkgInfo.attr : ''
            });

            if (pkgInfo.installDepList && pkgInfo.installDepList.length !== 0) {
                var installDepList = pkgInfo.installDepList.map(function (depPkg) {
                    return {
                        name: depPkg.packageName,
                        os: pkgInfo.os,
                        parentPkg: { name: pkgInfo.name, attr: (pkgInfo.attr === 'root' ? pkgInfo.attr : '') }
                    };
                });

                depPackageList =
                    getDependentPackages(installDepList, pkgOS, remoteSnapshot, depPackageList, monitor);
            }
        }
    });

    return depPackageList;
}


function generateInstallerInfo(metaPkgs, repo, distName, snapshot, monitor, callback) {
    async.waterfall([
        function (cb) {
            var remoteSnapshot = null;

            repo.searchSnapshots({
                name: snapshot.name,
                repoType: 'tizen',
                distName: distName
            },
            function (err, snapshots) {
                if (!err && snapshots.length !== 0) {
                    remoteSnapshot = snapshots[0];
                    remoteSnapshot.distName = distName;
                }
                cb(err, remoteSnapshot);
            });
        },
        function (remoteSnapshot, cb) {
            var depPkgList = getDependentPackages(metaPkgs, null, remoteSnapshot, null, monitor);
            var filteredPkgList = _.uniq(_.pluck(depPkgList, 'name'));
            var depPackages = [];

            _.each(filteredPkgList, function (pkgName) {
                var matchedPkg = _.findWhere(depPkgList, {name: pkgName});
                depPackages.push(matchedPkg);
            });

            cb(null, depPackages);
        }
    ],
    function (err, results) {
        callback(err, results);
    });
}


function generateInstallerImage(repo, distName, targetOS, snapshot, installDir, opts, monitor, callback) {

    // check workspace directory
    var workspace = process.cwd();
    if (opts.workspace) {
        workspace = opts.workspace;
    }

    // calculate target meta packages to install
    var targetPkgs = getMetaPackagesToInstall(targetOS, snapshot, opts.includeList, opts.excludeList, monitor);
    if (targetPkgs.length === 0) {
        return callback(new DError('TIZENDEPLOY005'));
    }

    if (!isValidMeta(snapshot, targetPkgs, monitor)) {
        return callback(new DError('TIZENDEPLOY014'));
    }

    // get host information
    var host = Tutils.getTargetOS(os.platform(), os.arch());

    var installerInfo = [];

    async.series([
        // install target packages
        function (cb) {
            monitor.updateProgress('Installing target meta packages: ' +
                targetPkgs.map(function (e) {
                    return e.name + '(' + e.os + ')';
                }).join(','));
            var shortcutMonitor = new ProgressMonitor({
                onProgress: function (info, cb) {
                    if (info.log.indexOf('##SHORTCUT') >= 0 &&
                        info.log.indexOf('echo') === -1) {
                        processShortcutInfo(info.log, installDir);
                    }
                    monitor.updateProgress(info);
                    cb(null);
                }
            });

            Installer.installMultiplePackages(targetPkgs, installDir,
                repo, distName, host, {
                    skipMetaDep: true,
                    snapshotName: snapshot.name
                }, shortcutMonitor, cb);
        },
        // generate image information
        function (cb) {
            monitor.updateProgress('Generating repository.info...');
            generateRepositoryInfo(installDir, repo, distName, snapshot, opts, cb);
        },
        // zip into image files
        function (cb) {
            monitor.updateProgress('Compressing installed directory...');
            Zip.compress(path.join(workspace, 'tizen-sdk.zip'), installDir, {}, cb);
        },
        function (cb) {
            Zip.getZipFileInformation(path.join(workspace, 'tizen-sdk.zip'), function (err, info) {
                if (err) {
                    cb(err);
                } else {
                    var zipInfo = 'TOTAL_UNZIP_SIZE=' + info.uncompressedSize + os.EOL +
                        'TOTAL_UNZIP_FILE_COUNT=' + info.numberOfFiles + os.EOL;

                    fs.writeFile(path.join(workspace, 'zipfile.info'), zipInfo, cb);
                }
            });
        },
        function (cb) {
            // Clean tempWorkspace
            monitor.updateProgress('Cleaning up installed directory...');
            utils.removePathIfExist(installDir, cb);
        },
        function (cb) {
            monitor.updateProgress('Getting detailed information for installer...');
            generateInstallerInfo(targetPkgs, repo, distName, snapshot, monitor, function (err, results) {
                if (err) {
                    monitor.updateProgress('ERROR - Getting detailed information for installer.' + err);
                } else {
                    installerInfo = {
                        pkgList: _.sortBy(results, 'name'),
                        isCLI: (opts.isCLI ? true : false),
                        repoUrl: opts.orginRepoURL,
                        includeList: opts.includeList,
                        excludeList: opts.excludeList,
                        category: 'installer'
                    };
                }
                cb(err);

            });
        }
    ],
    function (err) {
        callback(err, installerInfo);
    });
}


function getMetaPackagesToInstall(targetOS, snapshot, includeList, excludeList, monitor) {
    var osPkgs = snapshot.osPackages[targetOS];

    var rootMetaPkgs = getRootMetaPackages(targetOS, osPkgs);
    if (!excludeList) {
        excludeList = [];
    }
    if (!includeList || includeList.length === 0) {
        includeList = _.clone(rootMetaPkgs);
    }
    monitor.updateProgress('include list :');
    monitor.updateProgress('  ' + includeList.toString());
    monitor.updateProgress('exclude list :');
    monitor.updateProgress('  ' + excludeList.toString());
    // gather target meta packages
    var targetPkgNames = [];
    rootMetaPkgs.forEach(function (pkgName) {
        monitor.updateProgress('dependency of ' + pkgName);
        var selected = getMetaPkgsNamesToInstall(pkgName, targetOS, osPkgs,
            includeList, excludeList, false, monitor);
        selected.forEach(function (e) {
            if (targetPkgNames.indexOf(e) < 0) {
                targetPkgNames.push(e);
            }
        });
    });

    // sort by install order
    var pkgs = targetPkgNames.map(function (pkgName) {
        return osPkgs[pkgName];
    });
    return Package.sortPackagesByPartialInstallOrder(pkgs).map(function (e) {
        return {
            name: e.name,
            os: targetOS
        };
    });
}
module.exports.getMetaPackagesToInstall = getMetaPackagesToInstall;

// meta rule : install dependent packages of a meta package allows meta or real package type. However, both types cannot be together in the install dependent package field.
function isValidMeta(snapshot, metaList, monitor) {
    return _.every(metaList, function (meta) {
        var deps = Package.getInstallDepsOfPackages([snapshot.osPackages[meta.os][meta.name]], meta.os);
        var depMetas = _.filter(deps, function (dep) {

            return snapshot.osPackages[dep.os][dep.name].attr === 'root';
        });
        var valid = false;
        if (deps.length === depMetas.length || depMetas.length === 0) {
            valid = true;
        } else {
            monitor.updateProgress('  * Meta package ' + meta.name + ':' + meta.os + ' has mixed package type in install dependent package list');
        }
        return valid;
    });
}

function getRootMetaPackages(targetOS, osPkgs) {
    var allMetaPkgNames = Object.keys(osPkgs).filter(function (e) {
        return (osPkgs[e].attr === 'root');
    });

    var result = _.clone(allMetaPkgNames);
    allMetaPkgNames.forEach(function (e) {
        var pkg = osPkgs[e];
        pkg.installDepList.filter(function (dep) {
            return (osPkgs[dep.packageName].attr === 'root');
        }).forEach(function (dep) {
            var idx = result.indexOf(dep.packageName);
            if (idx >= 0) {
                result.splice(idx, 1);
            }
        });
    });

    return result;
}


function getMetaPkgsNamesToInstall(pkgName, targetOS, osPkgs,
    includeList, excludeList, parentIncluded, monitor) {
    var result = [];

    if (excludeList && excludeList.indexOf(pkgName) >= 0) {
        return [];
    }

    if (!parentIncluded && includeList &&
        includeList.indexOf(pkgName) >= 0) {
        parentIncluded = true;
    }

    if (parentIncluded) {
        result.push(pkgName);
    }

    var deps = Package.getInstallDepsOfPackages([osPkgs[pkgName]], targetOS);

    deps.forEach(function (dep) {
        var depPkg = osPkgs[dep.name];
        if (depPkg.attr !== 'root') {
            return;
        }

        var selected = getMetaPkgsNamesToInstall(dep.name, targetOS, osPkgs,
            includeList, excludeList, parentIncluded, monitor);
        selected.forEach(function (e) {
            if (result.indexOf(e) < 0) {
                result.push(e);
            }
        });
    });

    if (result.length > 0) {
        monitor.updateProgress(pkgName + ': -> ');
        monitor.updateProgress('   ' + result.toString());
    }
    if (!parentIncluded && result.length > 0) {
        result.push(pkgName);
    }

    return result;
}


function processShortcutInfo(shortcutInfoStr, installDir) {
    var shortcutInfoFile = path.join(installDir, '.info', 'shortcut.info');

    var toks = shortcutInfoStr.trim().split('=');
    var shortcutKey = toks[0].substring(2);
    var shortcutValue = toks.slice(1, 1 + toks.length).join('=');
    if (shortcutKey === 'SHORTCUT_FILE_PATH') {
        shortcutKey = 'SHORTCUT_FILE_NAME';
        shortcutValue = path.basename(shortcutValue);
    } else if (shortcutKey === 'SHORTCUT_EXECUTE_FILE_PATH' ||
        shortcutKey === 'SHORTCUT_ICON_FILE_PATH') {
        shortcutValue = shortcutValue.replace(installDir + path.sep, '');
    } else if (shortcutKey === 'SHORTCUT_END') {
        // add empty line
        fs.appendFileSync(shortcutInfoFile, os.EOL);
        return;
    } else { // SHORTCUT_NAME, SHORTCUT_COMMENT
        // do nothing
    }

    if (!fs.existsSync(shortcutInfoFile)) {
        fs.writeFileSync(shortcutInfoFile, shortcutKey + '=' + shortcutValue + os.EOL);
    } else {
        fs.appendFileSync(shortcutInfoFile, shortcutKey + '=' + shortcutValue + os.EOL);
    }
}


function generateRepositoryInfo(installDir, repo, distName, snapshot, opts, callback) {
    var imgInfo;
    async.waterfall([
        function (cb) {
            repo.getRepositoryURL(cb);
        },
        function (serverURL, cb) {
            imgInfo = 'Repository: ' + serverURL + os.EOL +
                'Distribution: ' + distName + os.EOL +
                'Installed-Snapshot: ' + snapshot.name + os.EOL;
            if (opts.orginRepoURL) {
                var originDistName = path.basename(opts.orginRepoURL);
                var originServerURL = path.dirname(opts.orginRepoURL);
                imgInfo = 'Repository: ' + originServerURL + os.EOL +
                    'Distribution: ' + originDistName + os.EOL;
            }
            cb(null);
        },
        function (cb) {
            repo.searchDistributions({
                repoType: 'tizen',
                distName: distName
            }, cb);
        },
        function (rsts, cb) {
            if (rsts[0].uid) {
                imgInfo += 'Distribution-ID : ' + rsts[0].uid;
            }

            fs.writeFile(path.join(installDir, '.info', 'repository.info'), imgInfo, cb);
        }], function (err) {
        callback(err);
    });
}


function createInstaller(repo, distName, targetOS, snapshot, opts, monitor, callback) {
    // check workspace directory
    var workspace = process.cwd();
    if (opts.workspace) {
        workspace = opts.workspace;
    }

    // get host information
    var host = Tutils.getTargetOS(os.platform(), os.arch());

    // get installDir for installer-base
    var installerBaseDir = path.join(workspace, 'installer-base');
    var installerBinPath = null;

    installerBinPath = path.join(installerBaseDir, 'installer', (opts.isCLI ? 'cli' : 'ui'));

    var scriptPath = null;
    if (os.platform() === 'win32') {
        scriptPath = path.join(installerBinPath, INST_GEN_SCRIPT + '.BAT');
    } else {
        scriptPath = path.join(installerBinPath, INST_GEN_SCRIPT);
    }

    async.series([
        // install target packages
        function (cb) {
            monitor.updateProgress('Installing \'installer\' package...');
            Installer.installSinglePackage('installer', targetOS,
                installerBaseDir, repo, distName, host, {
                }, monitor, cb);
        },
        // move image file
        function (cb) {
            FileSystem.move(path.join(workspace, 'tizen-sdk.zip'),
                path.join(installerBinPath, 'tizen-sdk.zip'), cb);
        },
        // move image file info
        function (cb) {
            FileSystem.move(path.join(workspace, 'zipfile.info'),
                path.join(installerBinPath, 'zipfile.info'), cb);
        },
        // check generator script
        function (cb) {
            fs.exists(scriptPath, function (exists) {
                if (!exists) {
                    cb(new DError('TIZENDEPLOY010'));
                } else {
                    cb(null);
                }
            });
        },
        // add "+x" attribute to script files
        function (cb) {
            fs.chmod(scriptPath, '777', cb);
        },
        // execute generator script
        function (cb) {
            monitor.updateProgress('Executing the script of generating installer...');
            var env = _.clone(process.env);
            if (opts.releaseName) {
                env.RELEASE_NAME = opts.releaseName;
            }

            if (opts.installerTitle) {
                env.INSTALLER_TITLE = opts.installerTitle;
            }

            Process.create(scriptPath,
                [],
                {
                    cwd: installerBinPath,
                    env: env
                },
                {
                    onStdout: function (line) {
                        monitor.updateProgress('   ' + line);
                    },
                    onStderr: function (line) {
                        monitor.updateProgress('   ' + line);
                    },
                    onExit: function (code) {
                        if (code !== 0) {
                            monitor.updateProgress({
                                log: ' - script failed with code : ' + code,
                                logType: 'error'
                            });
                            var error = new Error('Executing process exited with code ' + code);
                            cb(error);
                        } else {
                            cb(null);
                        }
                    }
                });
        }, function (cb) {
            monitor.updateProgress('Moving result files to workspace directory...');
            async.each(['installer.bin', 'installer.dmg', 'installer.exe'],
                function (fileName, cb1) {
                    var filePath = path.join(installerBinPath, fileName);
                    var fileToks = fileName.split('.');
                    var targetPath = path.join(workspace,
                        fileToks[0] + '_' + targetOS + '.' + fileToks[1]);
                    if (opts.isCLI) {
                        targetPath = path.join(workspace,
                            'cli-' + fileToks[0] + '_' + targetOS + '.' + fileToks[1]);
                    }
                    if (fs.existsSync(filePath)) {
                        FileSystem.move(filePath, targetPath, cb1);
                    } else {
                        cb1(null);
                    }
                }, cb);
        }
    ],
    function (err) {
        callback(err);
    });
}

