/**
 * package-installer.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';

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

var Process = require('../dibs.core/process.js');
var Zip = require('../dibs.core/zip.js');
var FileSystem = require('../dibs.core/filesystem.js');
var utils = require('../../lib/utils.js');
var util = require('./util.js');

var PACKAGE_INFO_DIR = '.info';
var INSTALLED_PKGLIST_FILE = 'installedpackage.list';


module.exports.installLocalPackages = installMultipleLocalPackages;
function installMultipleLocalPackages(pkgPathList, targetDir, hostOS, snapshot, distPath, options, monitor, callback) {

    var installedPkgs = [];
    var localPkgInfos = [];

    async.waterfall([
        function (cb) {
            monitor.updateProgress(' # - Check if installed packages exist or not in target directory');
            if (options.clean === true) {
                // remove targetDir if clean option is given
                utils.removePathIfExist(targetDir, function (err) {
                    if (err) {
                        cb(err);
                    } else {
                        extfs.mkdirs(targetDir, function (err1) {
                            cb(err1);
                        });
                    }
                });
            } else {
                // read installed packages from targetDir
                readInstalledPkgs(targetDir, function (err, results) {
                    if (!err) {
                        installedPkgs = results;
                    }
                    cb(null);
                });
            }
        },
        function (cb) {
            readLocalPkgs(pkgPathList, function (err1, results) {
                if (!err1) {
                    localPkgInfos = results;
                }
                cb(err1);
            });
        },
        function (cb) {
            var accInstalledPkgs = _.clone(installedPkgs);

            async.eachSeries(localPkgInfos, function (localPkg, cb1) {
                var pkg = localPkg.pkgInfo;

                installSingleLocalPackage(pkg.name, pkg.os, hostOS, targetDir, localPkgInfos, accInstalledPkgs,
                    snapshot, distPath, null, options, monitor,
                    function (err1) {
                        if (err1) {
                            monitor.updateProgress(' # - installing ' + pkg.name + ' failure from local');
                        }
                        cb1(err1);
                    });
            }, cb);
        }
    ],
    function (err) {
        callback(err);
    });
}


function installSingleLocalPackage(pkgName, pkgOS, hostOs, targetDir, localPkgInfos, accInstalledPkgs,
    snapshot, distPath, parentPkg, options, monitor, callback) {
    var pkgInfo = null;
    var pkgPath = null;
    var isLocalPkg = false;

    monitor.updateProgress(' # - Start package install process... ' + pkgName);

    // get package info from local or repo
    // if it exists in local pkgs, check it in local, otherwise check it in repo
    var results = localPkgInfos.filter(function (lpkg) {
        return (lpkg.pkgInfo !== null && lpkg.pkgInfo.name === pkgName &&
            lpkg.pkgInfo.os === pkgOS);
    });

    if (results.length > 0) {
        isLocalPkg = true;
        // get latest version
        pkgInfo = results[0].pkgInfo;
        pkgPath = results[0].path;
        for (var i in results) {
            if (util.package.compareVersion(pkgInfo.version, results[i].pkgInfo.version) < 0) {
                pkgInfo = results[i].pkgInfo;
                pkgPath = results[i].path;
            }
        }
    } else {
        if (snapshot && snapshot.osPackages[pkgOS] !== undefined) {
            pkgInfo = snapshot.osPackages[pkgOS][pkgName];
            if (utils.isURL(distPath)) {
                pkgPath = distPath + '/' + pkgInfo.path;
            } else {
                pkgPath = path.join(distPath, pkgInfo.path);
            }
        }
    }

    if (options.skipMetaDep && pkgInfo.attr === 'root' &&
        parentPkg && parentPkg.attr === 'root') {
        monitor.updateProgress(' # - \'' + pkgInfo.name + '(' + pkgInfo.os + ')\' is skipped by META dependency');
        return callback(null);
    }

    // compare package version between installed and installing package.
    var installedPkgs = accInstalledPkgs.filter(function (oldPkg) {
        return (oldPkg.name === pkgInfo.name &&
            (oldPkg.os === pkgInfo.os || oldPkg.osList.indexOf(pkgInfo.os) > 0) &&
            util.package.compareVersion(oldPkg.version, pkgInfo.version) >= 0);
    });
    if (installedPkgs.length > 0) {
        monitor.updateProgress(' # - \'' + pkgInfo.name + '(' + pkgInfo.os + ')\' is already installed!');
        return callback(null);
    }

    async.waterfall([
        function (cb) {
            if (pkgInfo.installDepList.length > 0) {
                // install install-depedent packages before target package.
                monitor.updateProgress(' # - Install install-dependent packages of \'' + pkgInfo.name + '\'');

                // NOTE. To accumulate 'accInstalledPkgs' , must use 'eachSeries'
                async.eachSeries(pkgInfo.installDepList, function (dep, cb1) {
                    var depOS = (dep.os === undefined) ? pkgInfo.os : dep.os;
                    installSingleLocalPackage(dep.packageName, depOS, hostOs, targetDir, localPkgInfos, accInstalledPkgs,
                        snapshot, distPath, pkgInfo, options, monitor, cb1);
                },
                function (err) {
                    cb(err);
                });
            } else {
                monitor.updateProgress(' # - Skip installing dependent packages... ' + pkgInfo.name);
                cb(null);
            }
        },
        function (cb) {
            if (!isLocalPkg) {
                utils.genTemp(function (err1, tempDir) {
                    snapshot.downloadPackages(distPath, [pkgInfo], tempDir, monitor, function (err1, results) {
                        pkgPath = results[0];
                        cb(err1);
                    });
                });
            } else {
                cb(null);
            }
        },
        function (cb) {
            // install target package
            monitor.updateProgress(' # - Install target package... ' + pkgInfo.name);
            installLocalPackage(pkgInfo, pkgPath, targetDir, accInstalledPkgs, hostOs, options, monitor, cb);
        }
    ],
    function (err) {
        callback(err);
    });
}


function installLocalPackage(pkgInfo, pkgPath, targetDir, installedPkgs, hostOS, options, monitor, callback) {
    var pkgList = _.clone(installedPkgs);
    var oldPkg = _.findWhere(installedPkgs, { name: pkgInfo.name, os: pkgInfo.os });

    async.waterfall([
        function (cb) {
            if (oldPkg) {
                monitor.updateProgress(' # - Uninstall package ' + oldPkg.name + ' ' + oldPkg.version);
                uninstall(oldPkg.name, targetDir, options, monitor, cb);
            } else {
                monitor.updateProgress(' # - Skip package uninstall because of no installed packages... ' + pkgInfo.name);
                cb(null);
            }
        },
        function (cb) {
            if (oldPkg) {
                pkgList = _.filter(installedPkgs, function (pkgInfo) {
                    return !(_.isEqual(pkgInfo, oldPkg));
                });

                installedPkgs = pkgList;
                monitor.updateProgress(' # - Remove' + oldPkg.name + ' from installed package list file');
                writeInstalledPkgs(targetDir, pkgList, cb);
            } else {
                monitor.updateProgress(' # - There is no installed packages! ... ' + pkgInfo.name);
                cb(null);
            }
        },
        function (cb) {
            if (options && options.extractCacheDir) {
                options.pkgInfo = pkgInfo;
            }

            monitor.updateProgress(' # - Install package... ' + pkgInfo.name);
            install(pkgPath, targetDir, hostOS, options, monitor, function (err, newPkg) {
                if (!err) {
                    installedPkgs.push(mergePkg(newPkg, pkgInfo));
                }
                cb(err, newPkg);
            });
        },
        function (newPkg, cb) {
            monitor.updateProgress(' # - Update ' + newPkg.name + ' installed package list file');
            writeInstalledPkgs(targetDir, installedPkgs, cb);
        }
    ],
    function (err) {
        callback(err);
    });
}


function mergePkg(highPriorityPkg, pkg) {
    var newPkg = _.clone(highPriorityPkg);
    if (highPriorityPkg.name === pkg.name &&
        highPriorityPkg.version === pkg.version &&
        highPriorityPkg.os === pkg.os) {
        _.each(pkg, function (value, key) {
            if (!newPkg[key]) {
                newPkg[key] = value;
            }
        });
        return newPkg;
    } else {
        return highPriorityPkg;
    }
}


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

    var rootMetaPkgs = getRootMetaPackages(targetOS, osPkgs);
    monitor.updateProgress(' # - ' + rootMetaPkgs.length + ' ROOT Meta Packages for ' + targetOS);

    if (!excludeList) {
        excludeList = [];
    }

    if (!includeList || includeList.length === 0) {
        includeList = _.clone(rootMetaPkgs);
    }

    monitor.updateProgress(' # - include list :');
    monitor.updateProgress('  ' + includeList.toString());
    if (!_.isEmpty(excludeList)) {
        monitor.updateProgress(' # - exclude list :');
        monitor.updateProgress('  ' + excludeList.toString());
    }

    // gather target meta packages
    var targetPkgNames = [];

    monitor.updateProgress(' # - Get dependent packages of root meta');
    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);
            }
        });
    });

    // monitor.updateProgress(targetPkgNames);

    // sort by install order
    var pkgs = targetPkgNames.map(function (pkgName) {
        return osPkgs[pkgName];
    });

    var results = util.package.sortPackagesByPartialInstallOrder(pkgs).map(function (e) {
        return {
            name: e.name,
            os: targetOS
        };
    });

    // monitor.updateProgress(results);
    return results;
}

function getRootMetaPackages(targetOS, osPkgs) {
    var allMetaPkgNames = Object.keys(osPkgs).filter(function (e) {
        return (osPkgs[e].attr === 'root');
    });
    var results = _.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 = results.indexOf(dep.packageName);
            if (idx >= 0) {
                results.splice(idx, 1);
            }
        });
    });

    return results;
}


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

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

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

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

    var deps = util.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 (results.indexOf(e) < 0) {
                results.push(e);
            }
        });
    });

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

    return results;
}


module.exports.installRemotePackages = installMultipleRemotePackages;
function installMultipleRemotePackages(pkgs, targetDir, hostOS, snapshot, distPath, options, monitor, callback) {
    // 1. remove targetDir if clean option is given
    // 2. read installed packages from targetDir
    // 3. get pkginfos if local packages are given
    // 4. get a snapshot from repository
    // 5. install a package using installSinglePackageInternal API
    callback(null);
}

function installSingleRemotePackage(pkgName, pkgOS, hostOS, targetDir, localPkgInfos, installedPkgs, options, callback) {
    callback(null);
}

function installRemotePackage(pkgInfo, snapshot, targetDir, installedPkgs, hostOS, options, callback) {
    callback(null);
}

module.exports.install = install;
module.exports.uninstall = uninstall;

// private functions
function install(pkgFilePath, installDir, hostOS, options, monitor, callback) {

    var pkgPath = pkgFilePath;
    var configPath = null;

    var osName = util.package.getInfoFromPackageFileName(path.basename(pkgPath)).os;
    var tempDir = null;
    var targetPkg = null;

    var PACKAGE_MANIFEST = 'pkginfo.manifest';
    var EXTRACT_LOG = 'extract.log';

    async.waterfall([
        function (cb) {
            utils.genTemp(cb);
        },
        function (temp, cb) {
            tempDir = temp;
            if (options.extractCacheDir) {
                // TODO:
                // fs.stat
                // utils.getCheckSum
                // extractFileUsingCache
                cb(null);
            } else {
                monitor.updateProgress(' # - Extracting... ' + pkgPath);
                extractFile(pkgPath, tempDir, options, monitor, cb);
            }
        },
        function (cb) {
            monitor.updateProgress(' # - Parse pkginfo.manifest... ' + path.basename(pkgPath));
            util.package.getPkgListFromFile(path.join(tempDir, PACKAGE_MANIFEST), cb);
        },
        function (pkgs, cb) {
            targetPkg = pkgs[0];
            configPath = path.join(installDir, PACKAGE_INFO_DIR, targetPkg.name);

            monitor.updateProgress(' # - Create package config path');
            extfs.mkdirp(configPath, function (err) {
                cb(err);
            });
        },
        function (cb) {
            var extractLogPath = path.join(tempDir, EXTRACT_LOG);
            var pkgListFilePath = path.join(configPath, targetPkg.name + '.list');

            monitor.updateProgress(' # - Save extracted file list into ' + pkgListFilePath);
            extfs.copy(extractLogPath, pkgListFilePath, cb);
        },
        function (cb) {
            // move data
            monitor.updateProgress(' # - Move extracted files from ' + tempDir + ' to ' + installDir);
            moveData(tempDir, installDir, monitor, cb);
            // process.exit(0);
        },
        function (cb) {
            if (osName === hostOS) {
                // runScript(tempDir, installDir, 'install', osName, cb);
                findScript(tempDir, 'install', osName, function (err, script) {
                    if (script && isScriptRunable(script)) {
                        monitor.updateProgress(' # - Execute install script for ' + targetPkg.name);
                        executeScript(script, installDir, monitor, cb);
                    } else {
                        monitor.updateProgress({ log: ' # - Install script does not exist... ' + targetPkg.name, logType: 'warn' });
                        cb(null);
                    }
                });
            } else {
                monitor.updateProgress({ log: ' # - Package OS (' + osName + ') is not as host (' + hostOS + ')', logType: 'warn' });
                monitor.updateProgress({ log: ' # - Skip install script file', logType: 'warn' });
                cb(null);
            }
        },
        function (cb) {
            findScript(tempDir, 'remove', osName, function (err, script) {
                cb(err, script);
            });
        },
        function (script, cb) {
            if (script) {
                monitor.updateProgress(' # - Move remove script file into ' + configPath);
                moveRemoveScript(script, configPath, osName, cb);
            } else {
                monitor.updateProgress({ log: ' # - Skip remove script file... ' + targetPkg.name, logType: 'warn' });
                cb(null);
            }
        }
    ],
    function (err) {
        utils.removePathIfExist(tempDir, function (err1) {
            if (err1) {
                monitor.updateProgress({ log: err1, logType: 'error' });
            }
            callback(err, targetPkg);
        });
    });
}


function uninstall(pkgName, installDir, options, monitor, callback) {
    monitor.updateProgress(' # - Uninstall package ' + pkgName);

    var configPath = path.join(installDir, PACKAGE_INFO_DIR, pkgName);
    var osName = util.getTargetOS(os.platform(), os.arch()).os;

    async.waterfall([
        function (cb) {
            monitor.updateProgress(' ## - Find remove sript');
            findScript(configPath, 'remove', osName, cb);
        },
        function (script, cb) {
            monitor.updateProgress(' ## - Execute remove sript');
            if (script && isScriptRunable(script)) {
                monitor.updateProgress(' ## - executing remove script...');
                executeScript(script, installDir, monitor, cb);
            } else {
                monitor.updateProgress({ log: ' ## - Remove script does not exist!', logType: 'warn' });
                cb(null);
            }
        },
        function (cb) {
            var listFile = path.join(configPath, pkgName + '.list');
            monitor.updateProgress(' ## - Remove package list file ' + listFile);
            removeFilesFromList(installDir, listFile, monitor, cb);
        },
        function (cb) {
            monitor.updateProgress(' ## - Remove a package');
            utils.removePathIfExist(configPath, cb);
        }
        /*,
        function (cb) {
            // remove package info in installedpackage.list
            monitor.updateProgress(' # - remove package info in installedpackage.list');
            var newPkgs = _.filter(installedPkgs, function (pkgInfo) {
                return !(_.isEqual(pkgInfo, pkg));
            });
            writeInstalledPkgs(installDir, newPkgs, cb);
        }
        */
    ],
    function (err) {
        callback(err);
    });
}


function findScript(srcDir, scriptType, osName, callback) {
    var candidates = [];

    var osCategory = util.getOSCategory(osName);
    var ext = (osCategory === 'windows' ? 'BAT' : 'sh');

    // ex) install.ubuntu-32.sh, install.windows-32.bat
    candidates.push(path.join(srcDir, scriptType + '.' + osName + '.' + ext));
    if (ext === 'sh') {
        candidates.push(path.join(srcDir, scriptType + '.' + osName));
    }

    // ex) install.linux.sh, install.windows.bat
    candidates.push(path.join(srcDir, scriptType + '.' + osCategory + '.' + ext));
    if (ext === 'sh') {
        candidates.push(path.join(srcDir, scriptType + '.' + osCategory));
    }

    // ex) install.sh, install.bat
    candidates.push(path.join(srcDir, scriptType + '.' + ext));
    if (ext === 'sh') {
        candidates.push(path.join(srcDir, scriptType));
    }

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


function isScriptRunable(scriptPath) {
    // XOR
    var ext = (path.extname(scriptPath) === '.BAT');
    var plat = (os.platform() === 'win32');
    return (ext && plat) || !(ext || plat);
}


function executeScript(script, targetDir, monitor, callback) {
    var lines = [];
    var shortcutScriptPath = path.join(__dirname, '../org.tizen.projects', 'scripts');

    var env = process.env;
    env.INSTALLED_PATH = targetDir;
    env.SDK_DATA_PATH = path.join(targetDir, 'user_data');
    env.USER_DATA_PATH = path.join(targetDir, 'user_data');
    env.INSTALLED_DIR_NAME = path.basename(targetDir);
    env.TSUDO = 'echo';
    env.SUPASS = '';

    if (os.platform() === 'win32') {
        env.MAKESHORTCUT_PATH = path.join(shortcutScriptPath, 'makeshortcut_windows.vbs');
        env.REMOVE_SHORTCUT = path.join(shortcutScriptPath, 'removeshortcut.vbs');
    } else if (os.platform() === 'darwin') {
        env.MAKESHORTCUT_PATH = path.join(shortcutScriptPath, 'makeshortcut_macos.sh');
        env.REMOVE_SHORTCUT = path.join(shortcutScriptPath, 'removeshortcut.sh');
    } else {
        env.MAKESHORTCUT_PATH = path.join(shortcutScriptPath, 'makeshortcut_linux.sh');
        env.REMOVE_SHORTCUT = path.join(shortcutScriptPath, 'removeshortcut.sh');
    }
    // NOTE. Default MUST be YES in buildsystem
    env.SKIP_SHORTCUT = 'YES';

    async.waterfall([
        function (cb) {
            fs.chmod(script, '0777', cb);
        },
        function (cb) {
            fs.access(env.MAKESHORTCUT_PATH, fs.F_OK, function (err) {
                if (err) {
                    monitor.updateProgress(' # - shortcut generation script does not exist');
                }
                cb(err);
            });
        },
        function (cb) {
            fs.access(env.REMOVE_SHORTCUT, fs.F_OK, function (err) {
                if (err) {
                    monitor.updateProgress(' # - shortcut remove script does not exist');
                }
                cb(err);
            });
        },
        function (cb) {
            Process.create(script,
                [],
                {
                    cwd: targetDir,
                    env: env
                },
                {
                    onStdout: function (line) {
                        lines.push(line);
                        monitor.updateProgress('   ' + line);
                    },
                    onStderr: function (line) {
                        monitor.updateProgress('   ' + line);
                    },
                    onExit: function (code) {
                        async.waterfall([
                            function (cb1) {
                                if (_.indexOf(lines, '##SHORTCUT_END=') >= 0 &&
                                    _.indexOf(lines, 'echo') === -1) {
                                    createShortcutInfo(lines, targetDir, monitor, function (err1) {
                                        cb1(err1);
                                    });
                                } else {
                                    cb1(null);
                                }
                            }
                        ],
                        function (err) {
                            if (code !== 0) {
                                monitor.updateProgress(' # - script failed with code : ' + code);
                                var error = new Error('Executing file(' + script + ') process exited with code ' + code);
                                cb(error);
                            } else {
                                cb(null);
                            }
                        });
                    }
                });
        }
    ], callback);
}


function createShortcutInfo(shortcutInfos, installDir, monitor, callback) {
    var shortcutInfoFile = path.join(installDir, '.info', 'shortcut.info');
    monitor.updateProgress(' # - Create shortcut info file... ' + shortcutInfoFile);

    var data = '';
    _.each(shortcutInfos, function (shortcutInfo) {
        var toks = shortcutInfo.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);
            data += (shortcutKey + '=' + shortcutValue + os.EOL);
        } else if (shortcutKey === 'SHORTCUT_EXECUTE_FILE_PATH' ||
            shortcutKey === 'SHORTCUT_ICON_FILE_PATH') {
            shortcutValue = shortcutValue.replace(installDir + path.sep, '');
            data += (shortcutKey + '=' + shortcutValue + os.EOL);
        } else if (shortcutKey === 'SHORTCUT_NAME' ||
            shortcutKey === 'SHORTCUT_COMMENT') {
            data += (shortcutKey + '=' + shortcutValue + os.EOL);
        } else {
            // do nothing
        }
    });

    data += os.EOL;

    fs.access(shortcutInfoFile, function (err) {
        if (err) {
            // create shortcut info file
            fs.writeFile(shortcutInfoFile, data, callback);
        } else {
            fs.appendFile(shortcutInfoFile, data, callback);
        }
    });
}


function extractFile(pkgFile, tempDir, options, monitor, callback) {

    async.waterfall([
        function (cb) {
            util.extractPackageFile(pkgFile, tempDir, {
                onStderr: function (line) {
                    monitor.updateProgress('   ' + line);
                },
                onExit: function (code) {
                    if (code !== 0) {
                        var error = new Error('Extract file process exited with code ' + code);
                        cb(error);
                    } else {
                        cb(null);
                    }
                }
            });
        },
        //Get zip information
        function (cb) {
            Zip.getZipFileInformation(pkgFile, function (err, info) {
                if (err) {
                    cb(err);
                } else {
                    var list = [];
                    var delim = /^data\//;

                    _.each(info.entry, function (l) {
                        if (l.match(delim) && l.split(delim)[1]) {
                            var filePath = l.split(delim)[1].trim();
                            if (os.platform() === 'win32') {
                                filePath = filePath.split('/').join('\\');
                            }
                            list.push(filePath);
                        }
                    });
                    cb(null, list);
                }
            });
        },
        // validate extract file list
        function (list, cb) {
            var locList = _.map(list, function (loc) {
                return path.join(tempDir, 'data', loc);
            });
            async.reject(locList, fs.exists, function (unmatchedList) {
                if (unmatchedList.length > 0) {
                    cb(new Error('Failed to validate extracted list : ' + unmatchedList.toString()), []);
                } else {
                    cb(null, list);
                }
            });
        },
        function (list, cb) {
            utils.readdirRecursive(path.join(tempDir, 'data'), {
                showDir: true,
                cutHead: true
            }, function (err, ls) {
                if (err) {
                    monitor.updateProgress(err);
                } else {
                    if (_.sortBy(ls).toString() === _.sortBy(list).toString()) {
                        monitor.updateProgress(' # - Validate extracted ' + pkgFile + ' file list done');
                        cb(null, list);
                    } else {
                        cb(new Error('list not matched ' + _.sortBy(ls).toString() + ' ::: ' + _.sortBy(list).toString()), []);
                    }
                }
            });
        },
        // Write extract file list
        function (list, cb) {
            fs.writeFile(path.join(tempDir, 'extract.log'), _.flatten(list).join('\n'), function (err) {
                cb(err, list);
            });
        }
    ], function (err) {
        callback(err);
    });
}

function removeFilesFromList(targetDir, listFile, monitor, callback) {
    var dirs = [];

    async.waterfall([
        function (cb) {
            fs.readFile(listFile, function (err, data) {
                if (err) {
                    monitor.updateProgress(err);
                    err = new Error('\'' + listFile + '\' does not exist.');
                }
                cb(err, data);
            });
        },
        // remove files
        function (data, cb) {
            async.each(_.compact(data.toString().split('\n')), function (offset, cb1) {
                var relativePath = offset.replace(/ ->.*/, '');
                var rmPath = path.join(targetDir, relativePath);

                fs.lstat(rmPath, function (err1, stat) {
                    //it's OK if file not exist
                    if (err1) {
                        monitor.updateProgress('\'' + rmPath + '\' does not exist.');
                    } else {
                        if (stat.isDirectory()) {
                            dirs.push(rmPath);
                            cb1(null);
                        } else {
                            fs.unlink(rmPath, cb1);
                        }
                    }
                });
            }, cb);
        },
        // remove dirs
        function (cb) {
            async.eachSeries(_.sortBy(dirs, function (dir) {
                return dir.length;
            }).reverse(), function (sdir, cb1) {
                fs.readdir(sdir, function (err, files) {
                    //it's OK if file not exist
                    if (err) {
                        cb1(null);
                    } else {
                        if (_.without(files, '.', '..').length === 0) {
                            fs.rmdir(sdir, cb1);
                        } else {
                            cb1(null);
                        }
                    }
                });
            }, cb);
        }
    ],
    function (err) {
        callback(err);
    });
}

function moveData(srcDir, targetDir, monitor, callback) {
    var dataPath = path.join(srcDir, 'data');

    var hostOS = os.platform();
    var installerFileName = 'inst-manager';
    if (hostOS === 'linux') {
        installerFileName += '.bin';
    } else if (hostOS === 'win32') {
        installerFileName += '.exe';
    } else if (hostOS === 'darwin') {
        installerFileName += '.dmg';
    } else {
        return callback(new Error('Cannot handle ' + hostOS));
    }

    fs.access(dataPath, fs.F_OK, function (err) {
        if (err) {
            monitor.updateProgress(' # - cannot access ' + dataPath);
            monitor.updateProgress(err);

            var srcInstaller = path.join(srcDir, installerFileName);
            var destInstaller = path.join(targetDir, installerFileName);

            FileSystem.copy(srcInstaller, destInstaller, { hardlink: false }, function (err1) {
                if (err1) {
                    monitor.updateProgress(' # - Copy failure ' + srcInstaller);
                    monitor.updateProgress(err1);
                }
                callback(err1);
            });
        } else {
            // copy all data from dataPath to targetDir
            monitor.updateProgress(' # - Copy all data from dataPath to targetDir');
            FileSystem.copy(dataPath, targetDir, { hardlink: false }, callback);
        }
    });
}

function moveRemoveScript(scriptPath, targetDir, osName, callback) {
    var removeScriptPath = path.join(targetDir, path.basename(scriptPath));

    async.series([
        function (cb) {
            utils.removePathIfExist(removeScriptPath, cb);
        },
        function (cb) {
            FileSystem.move(scriptPath, removeScriptPath, cb);
        },
        function (cb) {
            fs.chmod(removeScriptPath, '0755', cb);
        }
    ],
    function (err) {
        callback(err);
    });
}

function readInstalledPkgs(targetPath, callback) {
    var installedListPath = path.join(targetPath, PACKAGE_INFO_DIR, INSTALLED_PKGLIST_FILE);

    fs.exists(installedListPath, function (exist) {
        if (exist) {
            util.package.getPkgListFromFile(installedListPath, callback);
        } else {
            callback(new Error('\'' + installedListPath + '\' file does not exist!'), []);
        }
    });
}

function writeInstalledPkgs(targetPath, installedList, callback) {
    var installedListPath = path.join(targetPath, PACKAGE_INFO_DIR, INSTALLED_PKGLIST_FILE);

    fs.writeFile(installedListPath, util.package.pkgListString(installedList), callback);
}

function readLocalPkgs(pkgPaths, callback) {
    var pkgInfos = [];

    if (pkgPaths.length === 0) {
        callback(null, pkgInfos);
        return;
    }

    async.map(pkgPaths, function (lpath, cb) {
        var result = { pkgInfo: null, path: lpath };

        if (path.extname(lpath) === '.zip') {
            util.package.getPkgInfoFromPkgFile(lpath, function (err, pkg) {
                if (err) {
                    cb(err, result);
                } else {
                    result.pkgInfo = pkg;
                    cb(null, result);
                }
            });
        } else {
            cb(null, result);
        }
    },
    callback);
}
