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

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

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

var PACKAGE_INFO_DIR = '.info';
var PACKAGE_MANIFEST = 'pkginfo.manifest';
var INSTALLED_PKGLIST_FILE = 'installedpackage.list';
var EXTRACT_LOG = 'extract.log';

var MAKE_SHORTCUT_PATH_LINUX = path.join(__dirname, 'scripts', 'makeshortcut_linux.sh');
var MAKE_SHORTCUT_PATH_MACOS = path.join(__dirname, 'scripts', 'makeshortcut_macos.sh');
var MAKE_SHORTCUT_PATH_WINDOWS = path.join(__dirname, 'scripts', 'makeshortcut_windows.vbs');
var REMOVE_SHORTCUT_PATH_LINUX = path.join(__dirname, 'scripts', 'removeshortcut.sh');
var REMOVE_SHORTCUT_PATH_MACOS = path.join(__dirname, 'scripts', 'removeshortcut.sh');
var REMOVE_SHORTCUT_PATH_WINDOWS = path.join(__dirname, 'scripts', 'removeshortcut.vbs');

function installMultiplePackages(pkgs, targetDir, repo, distName, host, options, monitor, callback) {
    var localPkgInfos = [];
    var installedPkgs = [];
    var remoteSnapshot = null;

    async.series([
        // if clean options, re-create target directory,
        // otherwise, get installed packages
        function (cb) {
            monitor.updateProgress('Checking installed package(s)...', cb);
        },
        function (cb) {
            if (options.clean === true) {
                if (fs.existsSync(targetDir)) {
                    extfs.removeSync(targetDir);
                }
                extfs.mkdirsSync(targetDir);
                cb(null);
            } else {
                readInstalledPkgs(targetDir, function (err, iPkgs) {
                    if (!err) {
                        installedPkgs = iPkgs;
                    }
                    cb(null);
                });
            }
        },
        function (cb) {
            monitor.updateProgress('Checking local package(s)...', cb);
        },
        // get local packages if exists
        function (cb) {
            if (options.localPkgs !== undefined) {
                readLocalPkgs(options.localPkgs, function (err, pkgInfos) {
                    if (!err) {
                        localPkgInfos = pkgInfos;
                    }
                    cb(err);
                });
            } else {
                cb(null);
            }
        },
        function (cb) {
            monitor.updateProgress('Getting the latest snapshot of repository ...', cb);
        },
        // get latest snapshots of repository
        function (cb) {
            var sname = null;
            if (options.snapshotName) {
                sname = options.snapshotName;
            }
            repo.searchSnapshots({
                name: sname,
                repoType: 'tizen',
                distName: distName
            },
                function (err, snapshots) {
                    if (!err && snapshots.length > 0) {
                        remoteSnapshot = snapshots[0];
                        remoteSnapshot.distName = distName;
                    }
                    cb(err);
                });
        },
        // get latest snapshots of parent repository if specified
        function (cb) {
            if (options.parentRepoURL) {
                monitor.updateProgress('Getting the latest snapshot of parent repository ...');
                var parentServerURL = path.dirname(options.parentRepoURL);
                var parentDistName = path.basename(options.parentRepoURL);
                options.parentRepo = RemoteRepo.createRemoteRepo(parentServerURL, {
                    distName: parentDistName,
                    snapshotName: null
                });
                async.series([
                    function (cb1) {
                        monitor.updateProgress(' - Opening parent repository...', cb1);
                    },
                    function (cb1) {
                        options.parentRepo.open(null, cb1);
                    },
                    function (cb1) {
                        monitor.updateProgress(' - Getting latest snapshot from parent repository...', cb1);
                    },
                    function (cb1) {
                        options.parentRepo.searchSnapshots({
                            name: null,
                            repoType: 'tizen',
                            distName: parentDistName
                        }, function (err, snapshots) {
                            if (!err && snapshots.length > 0) {
                                options.parentRemoteSnapshot = snapshots[0];
                                options.parentRemoteSnapshot.distName = parentDistName;
                            }
                            cb1(err);
                        });
                    }], function (err) {
                    cb(err);
                });
            } else {
                cb(null);
            }
        },
        function (cb) {
            monitor.updateProgress('Installing package(s)...', cb);
        },
        // install packages
        function (cb) {
            // NOTE. To accumulate 'installedPkgs' , must use 'eachSeries'
            var accInstalledPkgs = _.clone(installedPkgs);
            async.eachSeries(pkgs,
                function (pkg, cb2) {
                    async.waterfall([
                        function (wcb) {
                            installSinglePackageInternal(pkg.name, pkg.os, host.os, targetDir,
                                localPkgInfos, accInstalledPkgs, repo, remoteSnapshot, null,
                                options, monitor, wcb);
                        },
                        function (wcb) {
                            monitor.updateProgress(' - installing ' + pkg.name + ' done', wcb);
                        }
                    ],
                        cb2);
                }, cb);
        }
    ],
    function (err) {
        if (!err) {
            monitor.updateProgress('Installing package(s) done!', callback);
        } else {
            monitor.updateProgress('Installing package(s) failed!');
            callback(err);
        }
    });
}
module.exports.installMultiplePackages = installMultiplePackages;


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

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

    async.map(pkgPaths,
        function (lpath, cb) {
            if (path.extname(lpath) === '.zip') {
                Package.getPkgInfoFromPkgFile(lpath, function (err, pkg) {
                    if (!err) {
                        cb(err, {
                            pkgInfo: pkg,
                            path: lpath
                        });
                    } else {
                        cb(err, {
                            pkgInfo: null,
                            path: lpath
                        });
                    }
                });
            } else {
                cb(null, {
                    pkgInfo: null,
                    path: lpath
                });
            }
        },
        callback);
}


// callback( err )
// accInstalledPkgs : accumulated list of installed packages
function installSinglePackageInternal(pkgName, pkgOS, hostOs, targetDir,
        localPkgInfos, accInstalledPkgs, repo, remoteSnapshot, parentPkg,
        options, monitor, callback) {
    var pkgInfo = null;
    var pkgPath = null;
    var isLocalPkg = false;

    monitor.updateProgress(' - installing...' + pkgName);

    // get package info from local or remote
    // if it exists in local pkgs, check it in local, otherwise check it in remote
    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 (Package.compareVersion(pkgInfo.version, results[i].pkgInfo.version) < 0) {
                pkgInfo = results[i].pkgInfo;
                pkgPath = results[i].path;
            }
        }
    } else {
        if (remoteSnapshot && remoteSnapshot.osPackages[pkgOS] !== undefined) {
            pkgInfo = remoteSnapshot.osPackages[pkgOS][pkgName];
        }
    }

    // if package not found, return
    monitor.updateProgress(' - checking package exists...' + pkgName);
    if (!pkgInfo) {
        if (options.parentRepoURL) {
            var newOptions = _.clone(options);
            delete newOptions['parentRepoURL'];

            installSinglePackageInternal(pkgName, pkgOS, hostOs, targetDir,
                localPkgInfos, accInstalledPkgs,
                options.parentRepo, options.parentRemoteSnapshot,
                parentPkg, options, monitor, callback);
        } else {
            var error = new Error('The package ' + pkgName + ' [' + pkgOS + '] is not in repository');
            callback(error);
        }
        // NOTE. MUST return
        return;
    }

    // if this and parent package are both META, then skip
    if (options.skipMetaDep && pkgInfo.attr === 'root' &&
        parentPkg && parentPkg.attr === 'root') {
        monitor.updateProgress(' - The package \'' + pkgName + '(' + pkgInfo.os + ')\' is skipped by META dependency!', callback);
        return;
    }

    // if it already exists in installed package list, then skip
    results = accInstalledPkgs.filter(function (installedPkg) {
        return (installedPkg.name === pkgInfo.name && (installedPkg.os === pkgInfo.os || installedPkg.osList.indexOf(pkgInfo.os) > 0) &&
            Package.compareVersion(installedPkg.version, pkgInfo.version) >= 0);
    });
    if (results.length > 0) {
        monitor.updateProgress(' - The package \'' + pkgName + '(' + pkgInfo.os + ')\' is already installed!', callback);
        return;
    }

    async.waterfall([
        // install its dependent pkgs, first
        function (cb) {
            if (pkgInfo.installDepList.length > 0) {
                monitor.updateProgress(' - installing install-dependent package(s)...' + pkgName);
                // NOTE. To accumulate 'accInstalledPkgs' , must use 'eachSeries'
                async.eachSeries(pkgInfo.installDepList,
                    function (dep, cb2) {
                        var depOS = (dep.os === undefined) ? pkgOS : dep.os;
                        installSinglePackageInternal(dep.packageName, depOS, hostOs,
                            targetDir, localPkgInfos, accInstalledPkgs,
                            repo, remoteSnapshot, pkgInfo, options, monitor, cb2);
                    },
                    function (err) {
                        cb(err);
                    });
            } else {
                cb(null);
            }
        },
        function (cb) {
            monitor.updateProgress(' - installing install-dependent pacakge(s) is done ! ...' + pkgName, cb);
        },
        // install myself
        function (cb) {
            if (isLocalPkg) {
                monitor.updateProgress(' - installing from local... ' + pkgName);
                installLocalPackage(pkgInfo, pkgPath, targetDir, accInstalledPkgs, hostOs,
                    options, monitor, cb);
            } else {
                monitor.updateProgress(' - installing from remote... ' + pkgName);
                installRemotePackage(pkgInfo, repo, remoteSnapshot, targetDir, accInstalledPkgs, hostOs,
                    options, monitor, cb);
            }
        }
    ],
        function (err) {
            callback(err);
        });
}


function installLocalPackage(pkgInfo, pkgPath, targetDir, installedPkgs, hostOs,
    options, monitor, callback) {
    var oldPkgs = installedPkgs.filter(function (pkg) {
        return (pkg.name === pkgInfo.name && pkg.os === pkgInfo.os);
    });

    async.waterfall([
        // uninstall old pkg
        function (cb) {
            if (oldPkgs.length > 0) {
                monitor.updateProgress(' - uninstalling old pakcage ' + oldPkgs[0].name + ' :version ' + oldPkgs[0].version);
                rawUninstall(oldPkgs[0], installedPkgs, targetDir, monitor, function (err, newPkgsInfo) {
                    if (!err) {
                        installedPkgs = newPkgsInfo;
                    }
                    cb(err);
                });
            } else {
                monitor.updateProgress(' - skipping removing installed package ', cb);
            }
        },
        function (cb) {
            monitor.updateProgress(' - installing package file...' + pkgPath, cb);
        },
        // install new pkg
        function (cb) {
            options.pkgInfo = pkgInfo;
            rawInstall(pkgPath, targetDir, hostOs, options, monitor, cb);
        },
        function (newPkg, cb) {
            installedPkgs.push(mergePkg(newPkg, pkgInfo));
            monitor.updateProgress(' - installing package file done...' + pkgPath, cb);
        },
        // update installed package list
        function (cb) {
            monitor.updateProgress(' - registering to installed package list...', cb);
        },
        function (cb) {
            writeInstalledPkgs(targetDir, installedPkgs, cb);
        }
    ],
    function (err) {
        callback(err);
    });
}
module.exports.forceInstallLocalPackage = installLocalPackage;


function installRemotePackage(pkgInfo, repo, remoteSnapshot, targetDir, installedPkgs, hostOs,
    options, monitor, callback) {

    var oldPkgs = installedPkgs.filter(function (pkg) {
        return (pkg.name === pkgInfo.name && pkg.os === pkgInfo.os);
    });

    var tempPath = null;
    var pkgPath = null;
    async.waterfall([
        // create temp directory
        function (cb) {
            monitor.updateProgress(' - generating temp dir for downloading remote package...', cb);
        },
        function (cb) {
            utils.genTemp(cb);
        },
        function (tempDir, cb) {
            tempPath = tempDir;
            monitor.updateProgress(' - ' + tempDir, cb);
        },
        // download package
        function (cb) {
            monitor.updateProgress(' - downloading remote package...' + pkgInfo.name + ':' + pkgInfo.os + ' from ' + remoteSnapshot.name, cb);
        },
        function (cb) {
            downloadPackage(repo, remoteSnapshot, pkgInfo.name, pkgInfo.os, tempPath, cb);
        },
        function (path2, cb) {
            pkgPath = path2;
            monitor.updateProgress(' - downloading done...' + pkgPath, cb);
        },
        // uninstall old pkg
        function (cb) {
            if (oldPkgs.length > 0) {
                monitor.updateProgress(' - uninstalling old package(s)...');
                rawUninstall(oldPkgs[0], installedPkgs, targetDir, monitor, function (err, newPkgsInfo) {
                    if (!err) {
                        installedPkgs = newPkgsInfo;
                    }
                    cb(null);
                });
            } else {
                cb(null);
            }
        },
        function (cb) {
            monitor.updateProgress(' - installing package...' + pkgPath, cb);
        },
        // install new pkg
        function (cb) {
            options.pkgInfo = pkgInfo;
            rawInstall(pkgPath, targetDir, hostOs, options, monitor, cb);
        },
        function (newPkg, cb) {
            installedPkgs.push(mergePkg(newPkg, pkgInfo));
            monitor.updateProgress(' - installing package file done...' + pkgPath, cb);
        },
        // update installed package list
        function (cb) {
            monitor.updateProgress(' - registering to installed package list...', cb);
        },
        function (cb) {
            writeInstalledPkgs(targetDir, installedPkgs, cb);
        }], function (err) {
        if (tempPath !== null) {
            extfs.removeSync(tempPath);
        }
        callback(err);
    });
}


function installSinglePackage(pkgName, targetOS, targetDir,
    repo, distName, host, options, monitor, callback) {
    installMultiplePackages([{
        name: pkgName,
        os: targetOS
    }],
        targetDir, repo, distName, host, options, monitor, callback);
}
module.exports.installSinglePackage = installSinglePackage;


function getConfigPath(targetPath, pkgName) {
    return path.join(targetPath, PACKAGE_INFO_DIR, pkgName);
}


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;
    }
}


// not aware windows os
// not aware tar format contents log
// extract pkgFile to tempDir and gen targetPath/.info/pkgName/pkgName.list
// do :
// 1. extract pkgFile to tempDir
// 2. gen pkgName.list file in targetPath/PACKAGE_INFO_DIR/pkgName/
function extractFile(pkgFile, tempDir, monitor, callback) {

    async.waterfall([
        function (cb) {
            Tutils.extractPackageFile(pkgFile, tempDir, {
                onStderr: function (line) {
                    monitor.updateProgress({
                        log: line,
                        logType: 'error'
                    });
                },
                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('extracted list validation failed : ' + unmatchedList.toString()), []);
                } else {
                    cb(null, list);
                }
            });
        },
        function (list, cb) {
            utils.readdirRecursive(path.join(tempDir, 'data'), {
                showDir: true,
                cutHead: true
            }, function (err, ls) {
                if (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);
            });
        }
        // Check extract file list
        //function (list, cb) {
        //    checkExtractFile(list, tempDir, cb);
        //}
    ], function (err) {
        callback(err);
    });
}

//function checkExtractFile(list, targetPath, callback) {
//    var delim = '';
//    var delimStr = null;
//    var unmatch = 0;
//
//    if (os.platform() === 'win32') {
//        delim = ' ' + utils.path2string('data' + path.sep);
//        delimStr = ' data' + path.sep;
//    } else {
//        delim = utils.path2string(path.join(targetPath, 'data') + path.sep);
//        delimStr = delim;
//    }
//
//    recursive(path.join(targetPath, 'data'), [EXTRACT_LOG], function (err, files) {
//        _.each(files, function (f) {
//            if (f.match(delim) && f.split(delimStr)[1]) {
//                f = f.split(delimStr)[1].trim();
//            }
//
//            if(_.indexOf(list, f) < 0) {
//                unmatch++;
//            }
//        });
//
//        if(unmatch !== 0) {
//            callback(new Error('Unmatched extracted file'));
//        } else {
//            callback(null);
//        }
//    });
//}


var cacheLocks = {};
var cacheQueue = [];
function extractFileUsingCache(pkgFile, targetDir,
    extractCacheDir, pkgInfo, monitor, callback) {

    // check if it exists in cache
    var cacheDirPath = path.join(extractCacheDir,
        pkgInfo.checksum + '.' + pkgInfo.size);

    if (fs.existsSync(cacheDirPath)) {
        if (cacheLocks[cacheDirPath] && cacheLocks[cacheDirPath].exclusive) {
            monitor.updateProgress('Waiting for extract-cache release...');
            setTimeout(function () {
                extractFileUsingCache(pkgFile, targetDir,
                    extractCacheDir, pkgInfo, monitor, callback);
            }, 1000);
        } else {
            if (cacheLocks[cacheDirPath]) {
                cacheLocks[cacheDirPath].refs++;
            } else {
                cacheLocks[cacheDirPath] = {
                    exclusive: false,
                    refs: 1
                };
            }
            monitor.updateProgress('Copying files from extract-cache...');
            FileSystem.copy(cacheDirPath, targetDir, {
                hardlink: true
            }, function (err) {
                cacheLocks[cacheDirPath].refs--;
                if (cacheLocks[cacheDirPath].refs <= 0) {
                    delete cacheLocks[cacheDirPath];
                }
                callback(err);
            });
        }
    } else {
        if (cacheLocks[cacheDirPath]) {
            monitor.updateProgress('Waiting for extract-cache release...');
            setTimeout(function () {
                extractFileUsingCache(pkgFile, targetDir,
                    extractCacheDir, pkgInfo, monitor, callback);
            }, 1000);
        } else {
            extractAndCopyFile(pkgFile, cacheDirPath, targetDir, monitor, function (err) {
                callback(err);
            });
        }
    }
}


function extractAndCopyFile(pkgFile, cacheDirPath, targetDir, monitor, callback) {
    cacheLocks[cacheDirPath] = {
        exclusive: true
    };

    // create dir if not exists
    if (!fs.existsSync(cacheDirPath)) {
        extfs.mkdirsSync(cacheDirPath);
    }

    // extract
    monitor.updateProgress('Extracting file into extract-cache...');
    extractFile(pkgFile, cacheDirPath, monitor, function (err) {
        if (err) {
            monitor.updateProgress('Extracting file failed!');
            utils.removePathIfExist(cacheDirPath, function (err1) {
                delete cacheLocks[cacheDirPath];
                callback(err);
            });
        } else {
            cacheQueue.push(cacheDirPath);
            cacheLocks[cacheDirPath].exclusive = false;
            cacheLocks[cacheDirPath].refs = 1;

            // check max number of cache dir
            if (cacheQueue.length > 50) {
                monitor.updateProgress('Extract-cache is full! Removing old caches...');
                handleExtractCacheFull();
            }

            // copy to target path
            FileSystem.copy(cacheDirPath, targetDir, {
                hardlink: true
            }, function (err) {
                cacheLocks[cacheDirPath].refs--;
                if (cacheLocks[cacheDirPath].refs <= 0) {
                    delete cacheLocks[cacheDirPath];
                }
                callback(err);
            });
        }
    });
}


function handleExtractCacheFull() {
    var deleteCnt = 0;

    // remove cache of unlocked.
    // remove maxinum, 10 cache entries
    for (var i = 0; i < cacheQueue.length; i++) {
        if (!cacheLocks[cacheQueue[i]] && deleteCnt < 10) {
            removeExtractCache(cacheQueue[i]);
            deleteCnt++;
        }
    }
}


function removeExtractCache(cacheDirPath) {
    cacheLocks[cacheDirPath] = {
        exclusive: true
    };
    utils.removePathIfExist(cacheDirPath, function (err) {
        delete cacheLocks[cacheDirPath];
        cacheQueue.splice(0, 1);
    });
}


// copy manifest and data foler to targetDir
// 1. copy srcDir/data to targetDir
function moveData(srcDir, targetDir, callback) {
    var dataPath = path.join(srcDir, 'data');
    fs.exists(dataPath, function (exist) {
        if (exist) {
            FileSystem.copy(dataPath, targetDir, {
                hardlink: false
            }, callback);
        } else {
            var targetDir2 = null;
            if (fs.existsSync(path.join(srcDir, 'inst-manager.bin'))) {
                targetDir2 = path.join(targetDir, 'inst-manager.bin');
                FileSystem.copy(path.join(srcDir, 'inst-manager.bin'), targetDir2, {
                    hardlink: false
                }, function (err) {
                    if (fs.existsSync(path.join(srcDir, 'inst-manager.dmg'))) {
                        targetDir2 = path.join(targetDir, 'inst-manager.dmg');
                        FileSystem.copy(path.join(srcDir, 'inst-manager.dmg'), targetDir2, {
                            hardlink: false
                        }, callback);
                    } else {
                        callback(err);
                    }
                });
            } else if (fs.existsSync(path.join(srcDir, 'inst-manager.exe'))) {
                targetDir2 = path.join(targetDir, 'inst-manager.exe');
                FileSystem.copy(path.join(srcDir, 'inst-manager.exe'), targetDir2, {
                    hardlink: false
                }, callback);
            } else {
                var error = new Error(srcDir + ' has no \'data\' folder!!');
                callback(error);
            }
        }
    });
}

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

    // check os name
    var ext = Tutils.getOSCategory(osName) === 'windows' ? 'BAT' : 'sh';
    candidates.push(path.join(srcDir, scriptType + '.' + osName + '.' + ext));
    if (ext === 'sh') {
        candidates.push(path.join(srcDir, scriptType + '.' + osName));
    }

    // check osCategory
    var osCategory = Tutils.getOSCategory(osName);
    candidates.push(path.join(srcDir, scriptType + '.' + osCategory + '.' + ext));
    if (ext === 'sh') {
        candidates.push(path.join(srcDir, scriptType + '.' + osCategory));
    }

    // check extension
    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);
}

// run script in targetDir
// set chmod +x
// INSTALLED_PATH = targetDir
function runScript(srcDir, targetDir, scriptType, osName, monitor, callback) {

    async.waterfall([
        function (cb) {
            findScript(srcDir, scriptType, osName, cb);
        },
        function (script, cb) {
            if (script && isScriptRunable(script)) {
                monitor.updateProgress(' - executing ' + scriptType + ' script...');
                executeScript(script, targetDir, monitor, cb);
            } else {
                monitor.updateProgress(' - no ' + scriptType + ' script exists!', cb);
            }
        }
    ],
    function (err) {
        callback(err);
    });
}


function executeScript(script, targetDir, monitor, callback) {
    async.waterfall([
        function (wcb) {
            fs.chmod(script, '0777', wcb);
        },
        function (wcb) {
            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.INSTALLMANAGER_UI = "TRUE";
            env.SUPASS = '';
            if (os.platform() === 'win32') {
                env.MAKESHORTCUT_PATH = MAKE_SHORTCUT_PATH_WINDOWS;
                env.REMOVE_SHORTCUT = REMOVE_SHORTCUT_PATH_WINDOWS;
            } else if (os.platform() === 'darwin') {
                env.MAKESHORTCUT_PATH = MAKE_SHORTCUT_PATH_MACOS;
                env.REMOVE_SHORTCUT = REMOVE_SHORTCUT_PATH_MACOS;
            } else {
                env.MAKESHORTCUT_PATH = MAKE_SHORTCUT_PATH_LINUX;
                env.REMOVE_SHORTCUT = REMOVE_SHORTCUT_PATH_LINUX;
            }
            // NOTE. Default MUST be YES in buildsystem
            env.SKIP_SHORTCUT = 'YES';

            Process.create(script,
                [],
                {
                    cwd: targetDir,
                    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 file(' + script + ') process exited with code ' + code);
                            wcb(error);
                        } else {
                            wcb(null);
                        }
                    }
                });
        }
    ], callback);
}


// move remove srcDir/script to targetDir
// chmod +x
// if not exist callback null
function moveRemoveScript(srcDir, targetDir, osName, callback) {
    findScript(srcDir, 'remove', osName, function (err, script) {
        if (script) {
            var removeScript = path.join(targetDir, path.basename(script));
            async.waterfall([
                function (wcb) {
                    fs.exists(removeScript, function (exist) {
                        if (exist) {
                            extfs.remove(removeScript, wcb);
                        } else {
                            wcb(null);
                        }
                    });
                },
                function (wcb) {
                    FileSystem.move(script, removeScript, wcb);
                },
                function (wcb) {
                    fs.chmod(removeScript, '0755', wcb);
                }
            ], callback);
        } else {
            // no remove script exist
            callback(null);
        }
    });
}


function rawUninstall(pkg, installedPkgs, installDir, monitor, callback) {
    var configPath = getConfigPath(installDir, pkg.name);
    var osName = Tutils.getTargetOS(os.platform(), os.arch()).os;

    async.waterfall([
        function (wcb) {
            monitor.updateProgress(' - uninstall package ' + pkg.name, wcb);
        },
        function (wcb) {
            // launch remove script
            monitor.updateProgress('rawUninstall - run remove script', wcb);
        },
        function (wcb) {
            runScript(configPath, installDir, 'remove', osName, monitor, function (err) {
                if (err) {
                    monitor.updateProgress(err.message);
                }
                wcb(err);
            });
        },
        function (wcb) {
            // remove package list under .info
            monitor.updateProgress('rawUninstall - remove package list under ' + path.join(configPath, pkg.name + '.list'), wcb);
        },
        function (wcb) {

            var listFile = path.join(configPath, pkg.name + '.list');
            removeFilesFromList(installDir, listFile, monitor, wcb);
        },
        function (wcb) {
            // remove a package
            monitor.updateProgress('rawUninstall - remove a package', wcb);
        },
        function (wcb) {

            FileSystem.remove(configPath, wcb);
        },
        function (wcb) {
            // remove package info in installedpackage.list
            monitor.updateProgress('rawUninstall - remove package info in installedpackage.list', wcb);
        },
        function (wcb) {
            var newPkgs = _.filter(installedPkgs, function (pkgInfo) {
                return !(_.isEqual(pkgInfo, pkg));
            });
            writeInstalledPkgs(installDir, newPkgs, wcb);
        }
    ],
    callback);
}

function rawInstall(pkgFile, installDir, hostOs, options, monitor, callback) {
    var ipkg = null;
    var tempDir = null;
    var configPath = null;
    var osName = Tutils.getInfoFromPackageFileName(path.basename(pkgFile)).os;

    async.waterfall([
        function (wcb) {
            monitor.updateProgress(' - generating temp dir for extracting...' + pkgFile, wcb);
        },
        function (wcb) {
            utils.genTemp(wcb);
        },
        function (temp, wcb) {
            tempDir = temp;
            monitor.updateProgress(' - ' + tempDir, wcb);
        },
        function (wcb) {
            if (options.extractCacheDir) {
                monitor.updateProgress(' - checking package file information...' + pkgFile);
                if (!options.pkgInfo.size) {
                    options.pkgInfo.size = fs.statSync(pkgFile).size;
                }
                if (!options.pkgInfo.checksum) {
                    utils.getCheckSum(pkgFile, function (err, checksum) {
                        if (!err) {
                            options.pkgInfo.checksum = checksum;
                        }
                        wcb(err);
                    });
                } else {
                    wcb(null);
                }
            } else {
                wcb(null);
            }
        },
        function (wcb) {
            if (options.extractCacheDir) {
                monitor.updateProgress(' - extracting package to cache directory... ' + pkgFile);
                extractFileUsingCache(pkgFile, tempDir,
                    options.extractCacheDir, options.pkgInfo, monitor, wcb);
            } else {
                monitor.updateProgress(' - extracting... ' + pkgFile);
                extractFile(pkgFile, tempDir, monitor, wcb);
            }
        },
        function (wcb) {
            monitor.updateProgress(' - extracting ' + pkgFile + ' done', wcb);
        },
        // get pkg
        function (wcb) {
            monitor.updateProgress(' - parsing pkginfo.manifest... ' + pkgFile, wcb);
        },
        function (wcb) {
            Package.getPkgListFromFile(path.join(tempDir, PACKAGE_MANIFEST), wcb);
        },
        function (pkg, wcb) {
            ipkg = pkg[0];
            monitor.updateProgress(' - preparing extraction log...' + pkgFile, wcb);
        },
        // set pkg extract log
        function (wcb) {
            configPath = getConfigPath(installDir, ipkg.name);
            extfs.mkdirp(configPath, function (err) {
                wcb(err);
            });
        },
        function (wcb) {
            monitor.updateProgress(' - creating package config path done', wcb);
        },
        function (wcb) {
            FileSystem.copy(path.join(tempDir, EXTRACT_LOG), path.join(configPath, ipkg.name + '.list'), wcb);
        },
        function (wcb) {
            monitor.updateProgress(' - saving extracted file list done', wcb);
        },
        function (wcb) {
            moveData(tempDir, installDir, wcb);
        },
        function (wcb) {
            monitor.updateProgress('  - moving extracted files from ' + tempDir + ' to ' + installDir + ' done', wcb);
        },
        function (wcb) {
            if (osName === hostOs) {
                runScript(tempDir, installDir, 'install', osName, monitor, wcb);
            } else {
                wcb(null);
            }
        },
        function (wcb) {
            if (osName === hostOs) {
                monitor.updateProgress(' - executing install script done', wcb);
            } else {
                monitor.updateProgress(' - WARN : package OS (' + osName + ') is not ' + hostOs, wcb);
            }
        },
        function (wcb) {
            moveRemoveScript(tempDir, configPath, osName, wcb);
        },
        function (wcb) {
            monitor.updateProgress(' - saving remove script done', wcb);
        }
    ],
    function (err) {
        if (tempDir !== null) {
            utils.removePathIfExist(tempDir, function (error) {
                if (error) {
                    monitor.updateProgress(error.message);
                }
                callback(err, ipkg);
            });
        } else {
            callback(err, ipkg);
        }
    });
}


function removeFilesFromList(targetDir, listFile, monitor, callback) {
    fs.exists(listFile, function (exist) {
        if (exist) {
            var dirs = [];
            async.waterfall([
                // read list file
                function (wcb) {
                    fs.readFile(listFile, wcb);
                },
                // remove files
                function (data, wcb) {
                    async.each(_.compact(data.toString().split('\n')), function (offset, ecb) {
                        var relativePath = offset.replace(/ ->.*/, '');
                        var rmPath = path.join(targetDir, relativePath);
                        fs.lstat(rmPath, function (err, stat) {
                            //it's OK if file not exist
                            if (err) {
                                monitor.updateProgress(' - ' + rmPath + ' does not exist', ecb);
                            } else {
                                if (stat.isDirectory()) {
                                    dirs.push(rmPath);
                                    ecb(null);
                                } else {
                                    fs.unlink(rmPath, ecb);
                                }
                            }
                        });
                    }, wcb);
                },
                // remove dirs
                function (wcb) {
                    async.eachSeries(_.sortBy(dirs, function (dir) {
                        return dir.length;
                    }).reverse(), function (sdir, ecb) {
                        fs.readdir(sdir, function (err, files) {
                            //it's OK if file not exist
                            if (err) {
                                ecb(null);
                            } else {
                                if (_.without(files, '.', '..').length === 0) {
                                    fs.rmdir(sdir, ecb);
                                } else {
                                    ecb(null);
                                }
                            }
                        });
                    }, wcb);
                }
            ], callback);
        } else {
            var error = new Error('File not exist : ' + listFile);
            callback(error);
        }
    });
}


// callback( err, path )
function downloadPackage(repo, remoteSnapshot, pkgName, pkgOS, targetPath, callback) {
    repo.downloadPackage(pkgName, {
        repoType: 'tizen',
        distName: remoteSnapshot.distName,
        snapshotName: remoteSnapshot.name,
        os: pkgOS,
        targetDir: targetPath
    }, callback);
}


function readInstalledPkgs(targetPath, callback) {
    var installedListPath = path.join(targetPath, PACKAGE_INFO_DIR, INSTALLED_PKGLIST_FILE);
    fs.exists(installedListPath, function (exist) {
        if (exist) {
            Package.getPkgListFromFile(installedListPath, callback);
        } else {
            callback(new Error('File not exist: ' + installedListPath), []);
        }
    });
}

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

    fs.writeFile(installedListPath, Package.pkgListString(installedList), function (err) {
        if (err) {
            console.error(err);
            callback(err, installedList);
        } else {
            callback(null, installedList);
        }
    });
}

function uninstallSinglePackage(pkgName, targetDir, monitor, callback) {

    async.waterfall([
        function (cb) {
            monitor.updateProgress('Getting installed package(s)...', cb);
        },
        function (cb) {
            readInstalledPkgs(targetDir, function (err, pkgsInfo) {
                cb(err, pkgsInfo);
            });
        },
        function (pkgsInfo, cb) {
            var packageInfo = pkgsInfo.filter(function (pkg) {
                return (pkgName === pkg.name);
            });

            if (_.isEmpty(packageInfo)) {
                cb(new Error('There is no \'' + pkgName + '\''), null);
            } else {
                rawUninstall(packageInfo[0], pkgsInfo, targetDir, monitor, cb);
            }
        }
    ],
    function (err) {
        if (err) {
            monitor.updateProgress('Failed to uninstall packages!');
            monitor.updateProgress(err.message);
            callback(err);
        } else {
            monitor.updateProgress('Succeeded to uninstall packages!', callback);
        }
    });
}
module.exports.uninstallSinglePackage = uninstallSinglePackage;

function showPackage(pkgName, targetDir, monitor, callback) {
    monitor.updateProgress('Getting package information ' + pkgName);

    readInstalledPkgs(targetDir, function (err, pkgsInfo) {
        if (err) {
            callback(err, null);
        } else {
            var packageInfo = pkgsInfo.filter(function (pkg) {
                return (pkgName === pkg.name);
            });

            if (_.isEmpty(packageInfo)) {
                callback(new Error('There is no \'' + pkgName + '\''), null);
            } else {
                callback(err, Package.pkgListString(packageInfo).trim());
            }
        }
    });
}
module.exports.showPackage = showPackage;

function listPackages(targetDir, monitor, callback) {

    monitor.updateProgress('Getting a list of packages from installed package(s)...');

    readInstalledPkgs(targetDir, function (err, pkgsInfo) {
        var packageList = '';

        _.each(pkgsInfo, function (pkgInfo) {
            var simplePkgInfo = pkgInfo.name + ' (' + pkgInfo.version + ')\n';
            packageList += simplePkgInfo;
        });

        callback(err, packageList.trim());
    });
}
module.exports.listPackages = listPackages;
