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

var dibs = require('../../core/dibs');
var dfs = require('../../plugins/dibs.dist-fs/dist-fs.js');
var Package = require('../org.tizen.common/package.js');
var Utils = require('../../lib/utils.js');
var TizenUtils = require('../org.tizen.common/tizen_utils.js');
var DError = require('../../core/exception.js');
var FileSystem = require('../dibs.core/filesystem.js');

module.exports.updateRefCountHash = updateRefCountHash;
module.exports.getPkgFileRefCountHash = getPkgFileRefCountHash;
module.exports.load = loadSnapshotInfo;
module.exports.loadSnapshot = loadSnapshot;
module.exports.loadRemoteSnapshot = loadRemoteSnapshot;
module.exports.loadChangelog = loadSnapshotChangeLog;
module.exports.save = saveSnapshot;
module.exports.saveTempSnapshot = saveTempSnapshot;
module.exports.create = createSnapshot;
module.exports.remove = removeSnapshots;
module.exports.removeSnapshotInfo = removeSnapshotInfo;

module.exports.appendPackages = appendPackagesToSnapshot;
module.exports.removePackages = removePackagesFromSnapshot;
module.exports.validate = validateSnapshot;
module.exports.prepareToDownload = prepareToDownload;

module.exports.getInstallDependentPackages = getInstallDependentPackages;
module.exports.getInstallDependentPackagesWithExcludeList = getInstallDependentPackagesWithExcludeList;

module.exports.loadRemote = loadRemoteSnapshots;
module.exports.getUpdatedPackages = getUpdatedPackages;
module.exports.getConflictPackages = getConflictPackages;
module.exports.getOutdatedPackages = getOutdatedPackages;
module.exports.getPackage = getPackage;
module.exports.checkUnmetDepsIfUpdated = checkUnmetDepsIfUpdated;


function TizenSnapshot(name) {
    var self = this;
    this.name = name;
    this.type = 'auto';
    this.time = null;
    this.path = null;
    this.changeLog = null;
    this.archivePackages = [];
    this.osPackages = {};
    this.isRemote = false;
    this.origin = null;
    this.uploader = null;
    this.newPkgs = [];
    this.updatedPkgs = [];
    this.removedPkgs = [];

    this.toString = function () {
        var originStr = '';
        if (self.origin) {
            originStr = 'origin : ' + self.origin + '\n';
        }
        return 'name : ' + self.name + '\n' +
            'time : ' + self.time + '\n' +
            'type : ' + self.type + '\n' +
            originStr +
            'path : ' + self.path + '\n';
    };
}


function getSnapshotInfoPath(distPath) {
    return path.join(distPath, 'snapshot.info');
}

/*
 * old : load all snapshot contents in snapshot Info list and changelogs
 * new : load snapshot list only
 */
function loadSnapshotInfo(distPath, callback) {
    var snapshotInfoPath = getSnapshotInfoPath(distPath);
    fs.exists(snapshotInfoPath, function (exist) {
        if (exist) {
            // load snapshot info
            fs.readFile(snapshotInfoPath, {
                encoding: 'utf8'
            },
                function (err, contents) {
                    if (err) {
                        return callback(err, null);
                    } else {
                        return callback(err, loadSnapshotInfoFromString(contents));
                    }
                });
        } else {
            return callback(new DError('TREPO010'), null);
        }
    });

}


function loadSnapshotInfoFromString(contents) {
    var lines = contents.split('\n');

    var snapshots = [];
    var newSnapshot = null;
    for (var i = 0; i < lines.length; i++) {
        var line = lines[i];
        var toks = line.split(/[: ]+/);
        if (toks[0] === 'name') {
            if (newSnapshot !== null) {
                snapshots.push(newSnapshot);
            }
            newSnapshot = new TizenSnapshot(toks[1]);
        } else if (toks[0] === 'time') {
            newSnapshot.time = toks[1];
        } else if (toks[0] === 'type') {
            newSnapshot.type = toks[1];
        } else if (toks[0] === 'path') {
            newSnapshot.path = toks[1];
        } else if (toks[0] === 'origin') {
            newSnapshot.origin = toks[1];
        } else {
            // ignore
        }
    }
    if (newSnapshot !== null) {
        snapshots.push(newSnapshot);
    }

    return snapshots;
}


function loadSnapshotChangeLog(changeLogPath, callback) {
    fs.readFile(changeLogPath, {
        encoding: 'utf8'
    }, function (err, changelog) {
        if (err) {
            return callback(null);
        } else {
            return callback(changelog);
        }
    });
}


function loadSnapshot(snapshotDirPath, callback) {

    // check
    fs.exists(snapshotDirPath, function (exist) {
        if (exist) {
            // create
            var newObj = new TizenSnapshot('Empty');
            async.waterfall([
                // load os info
                function (wcb) {
                    loadOsInfoFromFile(path.join(snapshotDirPath, 'os_info'), wcb);
                },
                // load os packages
                function (osList, wcb) {
                    loadOsPackages(osList, snapshotDirPath, wcb);
                },
                // load archive packages
                function (ospackages, wcb) {
                    newObj.osPackages = ospackages;
                    loadArchivePackagesFromFile(path.join(snapshotDirPath, 'archive_pkg_list'), wcb);
                },
                function (apackages, wcb) {
                    newObj.archivePackages = apackages;
                    wcb(null);
                }
            ], function (err) {
                callback(err, newObj);
            });
        } else {
            return callback(new DError('TREPO011', {
                path: snapshotDirPath
            }), null);
        }
    });

}


function loadOsInfoFromFile(osInfoPath, callback) {
    fs.readFile(osInfoPath, {
        encoding: 'utf8'
    },
        function (err, contents) {
            if (err) {
                return callback(null, TizenUtils.getOsList('Tizen-Source'));
            } else {
                return callback(err, loadOsInfo(contents));
            }
        });
}

function loadOsInfo(contents) {
    return contents.split('\n').filter(function (e) {
        return (e !== '');
    });
}


function loadOsPackagesFromUrl(snapshotDirURL, callback) {
    async.waterfall([
        function (wcb) {
            Utils.getTextFromUrl(snapshotDirURL + '/os_info', function (err, osInfoString) {
                if (err) {
                    // NOTE: For compatibility, if not exists, use default
                    return wcb(null, TizenUtils.getOsList('Tizen-Source'));
                } else {
                    return wcb(null, loadOsInfo(osInfoString));
                }
            });
        },
        function (osList, wcb) {
            async.map(osList, function (os, mcb) {
                async.waterfall([
                    function (wwcb) {
                        Utils.getTextFromUrl(snapshotDirURL + '/pkg_list_' + os, wwcb);
                    },
                    function (pkgListString, wwcb) {
                        Package.getPkgListFromString(pkgListString, wwcb);
                    }
                ], function (err, pkgList) {
                    mcb(err, _.object(_.map(pkgList, function (pkg) {
                        return pkg.name;
                    }), pkgList));
                });
            }, function (err, osPkglistList) {
                wcb(err, _.object(osList, osPkglistList));
            });
        }], callback);
}

function loadOsPackages(osList, snapshotDirPath, callback) {
    var newOsPackages = {};

    async.each(osList,
        function (os, cb) {
            var pkgListPath = path.join(snapshotDirPath, 'pkg_list_' + os);

            newOsPackages[os] = {};
            async.waterfall([
                function (wcb) {
                    fs.stat(pkgListPath, wcb);
                },
                function (stats, wcb) {
                    if (stats.size !== 0) {
                        Package.getPkgListFromFile(pkgListPath, wcb);
                    } else {
                        return wcb(null, []);
                    }
                },
                function (pkgList, wcb) {
                    _.each(pkgList, function (pkg) {
                        pkg.os = os;
                        newOsPackages[os][pkg.name] = pkg;
                    });
                    wcb(null);
                }
            ], function (err) {
                cb(err);
            });

        }, function (err) {
            callback(err, newOsPackages);
        });
}


function loadArchivePackagesFromFile(snapshotPath, callback) {
    fs.readFile(snapshotPath, {
        encoding: 'utf8'
    }, function (err, contents) {
        if (err) {
            // ignore error because archive_pkg_list file cannot be in existence.
            return callback(null, []);
        } else {
            return callback(err, loadArchivePackages(contents));
        }
    });
}

function loadArchivePackages(contents) {
    return contents.split('\n').filter(function (e) {
        return (e !== '');
    });
}


function createSnapshot(sname, src, options) {
    var timestamp = Utils.generateTimeStamp(true);
    var newObj = null;

    if (sname === null) {
        sname = timestamp;
    }

    newObj = new TizenSnapshot(sname);
    if (options && options.snapshotType === 'manual') {
        newObj.type = options.snapshotType;
        newObj.time = src.time;
        newObj.origin = src.name;
    } else {
        newObj.time = Utils.generateTimeStamp();
    }

    if (options && options.userEmail) {
        newObj.uploader = options.userEmail;
    }

    if (src) {
        for (var i = 0; i < src.archivePackages.length; i++) {
            var srcPkg = src.archivePackages[i];
            newObj.archivePackages.push(srcPkg);
        }

        for (var os in src.osPackages) {
            var pkgs = src.osPackages[os];

            newObj.osPackages[os] = {};
            for (var pname in pkgs) {
                var pkg = pkgs[pname];
                newObj.osPackages[os][pname] = pkg;
            }
        }
    }

    return newObj;
}


function saveSnapshot(snapshot, distPath, callback) {
    var snapshotDirPath = path.join(distPath, 'snapshots', snapshot.name);
    var changeLogDirPath = path.join(distPath, 'changes');
    var snapshots = [];
    // update path
    snapshot.path = '/snapshots/' + snapshot.name;

    async.series([
        function (scb) {
            // create directory
            fs.mkdir(snapshotDirPath, scb);
        },
        function (scb) {
            // save os info
            saveOsInfo(snapshot, snapshotDirPath, scb);
        },
        function (scb) {
            // save os packages
            saveOsPackages(snapshot, snapshotDirPath, scb);
        },
        function (scb) {
            // save archive packages
            saveArchivePackages(snapshot, snapshotDirPath, scb);
        },
        function (scb) {
            // save change log
            saveChangeLogs(snapshot, changeLogDirPath, scb);
        },
        function (scb) {
            // add snapshot
            loadSnapshotInfo(distPath, function (err, snapshotList) {
                snapshots = snapshotList;
                scb(err);
            });
        },
        function (scb) {

            var insertIdx = null;
            if (snapshot.type === 'manual') {
                var originSnapshots = snapshots.filter(function (e) {
                    return e.name === snapshot.origin;
                });
                if (originSnapshots.length >= 1) {
                    insertIdx = snapshots.indexOf(originSnapshots[0]) + 1;
                } else {
                    insertIdx = snapshots.length;
                }
            } else {
                insertIdx = snapshots.length;
            }
            snapshots.splice(insertIdx, 0, snapshot);

            // save snapshot info
            saveSnapshotInfo(snapshots, distPath, scb);
        },
        // copy snapshot files into root dir for compatibility if latest one
        function (scb) {
            if (snapshots.indexOf(snapshot) === snapshots.length - 1) {
                copySnapshotFilesToRoot(snapshot, distPath, scb);
            } else {
                return scb(null);
            }
        }
    ], function (err) {
        callback(err);
    });
}


function copySnapshotFilesToRoot(snapshot, distPath, callback) {
    var snapshotDirPath = path.join(distPath, 'snapshots', snapshot.name);

    async.series([
        // copy os_info
        function (cb) {
            FileSystem.copy(path.join(snapshotDirPath, 'os_info'),
                path.join(distPath, 'os_info'), cb);
        },
        // copy archive_pkg_list
        function (cb) {
            FileSystem.copy(path.join(snapshotDirPath, 'archive_pkg_list'),
                path.join(distPath, 'archive_pkg_list'), cb);
        },
        // copy pkg_list_*
        function (cb) {
            var osList = Object.keys(snapshot.osPackages);
            async.eachSeries(osList,
                function (os, cb1) {
                    FileSystem.copy(path.join(snapshotDirPath, 'pkg_list_' + os),
                        path.join(distPath, 'pkg_list_' + os), cb1);
                },
                cb);
        }], function (err) {
        callback(err);
    });
}


function saveTempSnapshot(snapshot, distPath, relateTempPath, callback) {
    var tempPath = path.join(distPath, relateTempPath);
    //update path
    snapshot.path = '/' + relateTempPath;

    async.waterfall([
        function (wcb) {
            //save infos
            saveOsInfo(snapshot, tempPath, wcb);
        },
        function (wcb) {
            saveOsPackages(snapshot, tempPath, wcb);
        },
        function (wcb) {
            saveArchivePackages(snapshot, tempPath, wcb);
        },
        function (wcb) {
            // add snapshot
            loadSnapshotInfo(distPath, wcb);
        },
        function (snapshots, wcb) {

            snapshots.unshift(snapshot);

            // save snapshot info
            saveSnapshotInfo(snapshots, distPath, wcb);
        }
    ], callback);
}


function saveOsInfo(snapshot, snapshotDirPath, callback) {
    var osList = Object.keys(snapshot.osPackages);
    fs.writeFile(path.join(snapshotDirPath, 'os_info'), osList.join('\n'),
        function (err) {
            callback(err);
        });
}


function saveOsPackages(snapshot, snapshotDirPath, callback) {
    var osPkgList = _.map(snapshot.osPackages, function (packages, os) {
        return [os, Package.pkgListString(packages)];
    });

    async.each(osPkgList, function (osPkg, ecb) {
        var os = osPkg[0];
        var contents = osPkg[1];
        fs.writeFile(path.join(snapshotDirPath, 'pkg_list_' + os), contents, ecb);
    }, function (err) {
        callback(err);
    });
}


function saveArchivePackages(snapshot, snapshotDirPath, callback) {
    fs.writeFile(path.join(snapshotDirPath, 'archive_pkg_list'), snapshot.archivePackages.join('\n'),
        function (err) {
            callback(err);
        });
}


function saveChangeLogs(snapshot, changeLogDirPath, callback) {
    snapshot.changeLog = '';
    // date , uploader
    var dateFormat = Utils.getTimeString(Utils.getDateFromTimeStamp(snapshot.time));
    if (snapshot.uploader) {
        snapshot.changeLog += dateFormat + ' <' + snapshot.uploader + '>\n';
    } else {
        snapshot.changeLog += dateFormat + ' <' + 'admin@user' + '>\n';
    }
    // new pkgs
    if (snapshot.newPkgs.length > 0) {
        appendNewPackagesToChangeLog(snapshot);
    }
    // removed pkgs
    if (snapshot.removedPkgs.length > 0) {
        appendRemovedPackagesToChangeLog(snapshot);
    }
    // update pkgs
    if (snapshot.updatedPkgs.length > 0) {
        appendUpdatedPackagesToChangeLog(snapshot);
    }

    // write to file
    fs.writeFile(path.join(changeLogDirPath, snapshot.name + '.log'), snapshot.changeLog,
        function (err) {
            callback(err);
        });
}


function appendNewPackagesToChangeLog(snapshot) {
    var pkgs = {};
    snapshot.newPkgs.forEach(function (e) {
        if (pkgs[e.name]) {
            pkgs[e.name].osList.push(e.os);
        } else {
            if (typeof e === 'string') {
                pkgs[e] = {};
            } else {
                var changelog = findChangeLogForVersion(e.changelog, e.version);
                pkgs[e.name] = {
                    version: e.version,
                    osList: [e.os],
                    changeLog: changelog
                };
            }
        }
    });

    snapshot.changeLog += '* New Packages\n';
    for (var pkgName in pkgs) {
        if (pkgs[pkgName].version) {
            snapshot.changeLog += ' - ' + pkgName + ' (' + pkgs[pkgName].version + ') [' + pkgs[pkgName].osList.join(',') + ']\n';
            snapshot.changeLog += pkgs[pkgName].changeLog + '\n';
        } else {
            snapshot.changeLog += ' - ' + pkgName + '\n';
        }
    }
}


function appendRemovedPackagesToChangeLog(snapshot) {
    var pkgs = {};
    snapshot.removedPkgs.forEach(function (e) {
        if (pkgs[e.name]) {
            pkgs[e.name].osList.push(e.os);
        } else {
            if (typeof e === 'string') {
                pkgs[e] = {};
            } else {
                pkgs[e.name] = {
                    version: e.version,
                    osList: [e.os]
                };
            }
        }
    });

    snapshot.changeLog += '* Removed Packages\n';
    for (var pkgName in pkgs) {
        if (pkgs[pkgName].version) {
            snapshot.changeLog += ' - ' + pkgName + ' (' + pkgs[pkgName].version + ') [' + pkgs[pkgName].osList.join(',') + ']\n';
        } else {
            snapshot.changeLog += ' - ' + pkgName + '\n';
        }
    }
}


function appendUpdatedPackagesToChangeLog(snapshot) {
    var pkgs = {};
    snapshot.updatedPkgs.forEach(function (e) {
        if (pkgs[e.name]) {
            pkgs[e.name].osList.push(e.os);
        } else {
            if (typeof e === 'string') {
                pkgs[e] = {};
            } else {
                var changelog = findChangeLogForVersion(e.changelog, e.version);
                pkgs[e.name] = {
                    version: e.version,
                    osList: [e.os],
                    changeLog: changelog
                };
            }
        }
    });

    snapshot.changeLog += '* Updated Packages\n';
    for (var pkgName in pkgs) {
        if (pkgs[pkgName].version) {
            snapshot.changeLog += ' - ' + pkgName + ' (' + pkgs[pkgName].version + ') [' + pkgs[pkgName].osList.join(',') + ']\n';
            snapshot.changeLog += pkgs[pkgName].changeLog + '\n';
        } else {
            snapshot.changeLog += ' - ' + pkgName + '\n';
        }
    }
}


function findChangeLogForVersion(changelogs, version) {
    if (!changelogs) {
        return '';
    }

    var logs = changelogs.filter(function (e) {
        return e.version === version;
    });
    if (logs.length > 0) {
        return logs[0].contents.split('\n').map(function (e) {
            return '    ' + e;
        }).join('\n');
    } else {
        return '';
    }
}


function saveSnapshotInfo(snapshots, distPath, callback) {
    var snapshotInfoPath = getSnapshotInfoPath(distPath);
    fs.writeFile(snapshotInfoPath, snapshots.join('\n'),
        function (err) {
            callback(err);
        });
}


function selectSnapshotInfo(snapshotNameList, snapshotInfoList) {
    var snapshotInfoHash = _.indexBy(snapshotInfoList, 'name');
    return _.map(snapshotNameList, function (name) {
        return snapshotInfoHash[name];
    });
}

function removeSnapshotInfo(snapshotNameList, snapshotInfoList) {
    var snapshotInfoHash = _.indexBy(snapshotInfoList, 'name');
    _.each(snapshotNameList, function (name) {
        snapshotInfoHash[name] = null;
    });
    return _.compact(_.values(snapshotInfoHash));
}

/*
 *
 */
function removeSnapshots(snapshotNameList, distPath, callback) { //-> callback(err, pkgFileRefCountHash)
    var removeSnapshotInfoList = [];
    var removePkgFileRefCountHash = {};
    var removedSnapshotInfoList = [];

    var loadDistSnapshotInfo = function (cb) {
        loadSnapshotInfo(distPath, cb);
    };
    var getRemovePkgFileRefCountHash = function (snapshotInfoList, cb) {
        removeSnapshotInfoList = selectSnapshotInfo(snapshotNameList, snapshotInfoList);
        removedSnapshotInfoList = removeSnapshotInfo(snapshotNameList, snapshotInfoList);
        getPkgFileRefCountHash(distPath, removeSnapshotInfoList, cb);
    };
    var saveRemovedSnapshotInfo = function (rPkgFileRefCountHash, cb) {
        removePkgFileRefCountHash = rPkgFileRefCountHash;
        saveSnapshotInfo(removedSnapshotInfoList, distPath, cb);
    };
    var removeDirs = function (cb) {
        var removePathList = _.map(removeSnapshotInfoList, function (sInfo) {
            return path.join(distPath, sInfo.path);
        });
        async.eachLimit(removePathList, 10, extfs.remove, cb);
    };
    async.waterfall([
        loadDistSnapshotInfo,
        getRemovePkgFileRefCountHash,
        saveRemovedSnapshotInfo,
        removeDirs
    ],
        function (err) {
            callback(err, removePkgFileRefCountHash);
        });
}


function appendPackagesToSnapshot(fileInfos, oldSnapshot, opts, callback) {
    var PATH_PREFIX = '/binary/';
    if (opts && opts.PATH_PREFIX) {
        PATH_PREFIX = opts.PATH_PREFIX;
    }

    var snapshotName = null;
    if (opts && opts.syncSnapshotName) {
        if (opts.syncPrefix) {
            snapshotName = opts.syncPrefix + opts.syncSnapshotName;
        } else {
            snapshotName = opts.syncSnapshotName;
        }
    }

    // create
    var newSnapshot = createSnapshot(snapshotName, oldSnapshot);
    if (opts && opts.isTemp) {
        newSnapshot.name = opts.TEMP_NAME;
        newSnapshot.type = 'temp';
    }

    if (opts && opts.userEmail) {
        newSnapshot.uploader = opts.userEmail;
    }

    // update
    for (var dfsPath in fileInfos) {
        var pkg = fileInfos[dfsPath];

        if (pkg !== null) {
            // if have compat OSs, set target OS from filename
            if (pkg.osList && pkg.osList.length > 1) {
                var pInfo = TizenUtils.getInfoFromPackageFileName(path.basename(dfsPath));
                pkg.os = pInfo.os;
            }

            // check os first
            var osList = TizenUtils.getEnvironmentNames('Tizen-Source');
            if (osList.indexOf(pkg.os) === -1) {
                callback(new DError('TREPO012', {
                    os: pkg.os
                }));
                return;
            }

            // check special charactor in change log
            if (!opts.noChangeLogCheck && pkg.changelog) {
                var latestChangeLog = findChangeLogForVersion(pkg.changelog, pkg.version);
                var result = checkChangeLog(latestChangeLog);
                if (result.err) {
                    return callback(result.err);
                }
            }

            if (opts.uploadCompat) {
                for (var i = 0; i < pkg.osList.length; i++) {
                    var os = pkg.osList[i];

                    // skip if there is no os environment
                    if (osList.indexOf(os) === -1) {
                        continue;
                    }

                    // check version & size & checksum
                    if (!opts.forceUpload && oldSnapshot && oldSnapshot.osPackages[os] &&
                        oldSnapshot.osPackages[os][pkg.name]) {

                        var oldPkg = oldSnapshot.osPackages[os][pkg.name];
                        if (Package.compareVersion(pkg.version, oldPkg.version) < 0) {
                            callback(new DError('TREPO013', {
                                new: pkg.version,
                                old: oldPkg.version
                            }));
                            return;
                        }

                        if (Package.compareVersion(pkg.version, oldPkg.version) === 0 &&
                            (pkg.size !== oldPkg.size || pkg.checksum !== pkg.checksum)) {

                            callback(new DError('TREPO024', {
                                pkgName: pkg.name
                            }));
                            return;
                        }
                    }

                    if (!newSnapshot.osPackages[os]) {
                        newSnapshot.osPackages[os] = {};
                    }

                    var newObj = _.clone(pkg);
                    // NOTE. for setting correct os when compat os
                    newObj.os = os;
                    newObj.path = PATH_PREFIX +
                        TizenUtils.getPackageFileNameFromInfo(pkg.name, pkg.version, os);

                    // add new or updated pkgs
                    if (newSnapshot.osPackages[os][pkg.name]) {
                        newSnapshot.updatedPkgs.push(newObj);
                    } else {
                        newSnapshot.newPkgs.push(newObj);
                    }

                    // set
                    newSnapshot.osPackages[os][pkg.name] = newObj;
                }
            } else {
                // check version
                if (!opts.forceUpload) {
                    var oldPkg = null;
                    if (oldSnapshot && oldSnapshot.osPackages[pkg.os]) {
                        oldPkg = oldSnapshot.osPackages[pkg.os][pkg.name];
                    }
                    if (oldPkg) {
                        var pkgVersionComp = Package.compareVersion(pkg.version, oldPkg.version);
                        if (pkgVersionComp < 0 || (!opts.forceSync && pkgVersionComp === 0)) {
                            callback(new DError('TREPO013', {
                                new: pkg.version,
                                old: oldPkg.version
                            }));
                            return;
                        }
                    }

                    if (opts.sync === 'direct') {
                        // except local package
                        if (oldPkg && oldPkg.origin === 'local') {
                            continue;
                        } else {
                            pkg.origin = 'remote';
                        }
                    } else {
                        pkg.origin = pkg.origin || 'local';
                    }
                }

                if (newSnapshot.osPackages[pkg.os] === undefined) {
                    newSnapshot.osPackages[pkg.os] = {};
                }

                // set attributes
                var newObj = _.clone(pkg);
                newObj.path = PATH_PREFIX +
                    TizenUtils.getPackageFileNameFromInfo(pkg.name, pkg.version, pkg.os);

                // add new or updated pkgs
                if (newSnapshot.osPackages[pkg.os][pkg.name]) {
                    newSnapshot.updatedPkgs.push(newObj);
                } else {
                    newSnapshot.newPkgs.push(newObj);
                }

                // set
                newSnapshot.osPackages[pkg.os][pkg.name] = newObj;
            }
        } else {
            var srcPkg = path.basename(dfsPath);
            if (newSnapshot.archivePackages.indexOf(srcPkg) === -1) {
                // add new
                newSnapshot.newPkgs.push(srcPkg);
                // set
                newSnapshot.archivePackages.push(path.basename(dfsPath));
            }
        }
    }

    // validate
    if (!opts.skipIntegrityCheck) {
        validateSnapshot(newSnapshot, function (err) {
            callback(err, newSnapshot);
        });
    } else {
        callback(null, newSnapshot);
    }
}


function removePackagesFromSnapshot(names, oldSnapshot, opts, callback) {

    // create
    var newSnapshot = createSnapshot(null, oldSnapshot);

    // update
    for (var i = 0; i < names.length; i++) {
        var pname = names[i];

        // first, search name in archive package list
        var idx = newSnapshot.archivePackages.indexOf(pname);
        if (idx > -1) {
            newSnapshot.removedPkgs.push(pname);
            newSnapshot.archivePackages.splice(idx, 1);
            continue;
        }

        // if not found, search it in os package list
        var found = false;
        for (var os in newSnapshot.osPackages) {
            // check os matched
            if (opts.os && opts.os !== os) {
                continue;
            }

            var pkg = newSnapshot.osPackages[os][pname];
            if (pkg) {
                newSnapshot.removedPkgs.push(pkg);
                delete (newSnapshot.osPackages[os])[pname];
                found = true;
            } else if (opts.os) { // if not found but os is defined, error
                callback(new DError('TREPO014', {
                    pkg: pname,
                    os: os
                }), null);
                return;
            } else {
                // ignore
            }
        }

        // if there no packages to be removed, error
        if (!found) {
            callback(new DError('TREPO014', {
                pkg: pname,
                os: 'any'
            }), null);
            return;
        }
    }
    // validate
    if (!opts.skipIntegrityCheck) {
        validateSnapshot(newSnapshot, function (err) {
            if (!err) {
                return callback(err, newSnapshot);
            } else {
                return callback(err, null);
            }
        });
    } else {
        return callback(null, newSnapshot);
    }
}


function validateSnapshot(snapshot, callback) {
    var errMsgs = [];
    // check only for nomal packages
    for (var os in snapshot.osPackages) {
        for (var pname in snapshot.osPackages[os]) {
            var pkg = snapshot.osPackages[os][pname];
            var result = findUnmetDependencies(pkg.installDepList, pkg.os, snapshot);
            if (result.length > 0) {
                errMsgs.push('- Invalid install dependency of ' + pkg.name + '(' + pkg.os +
                        '). The following package(s) must be in the distribution:' + result.join(','));
            }

            result = findUnmetDependencies(pkg.buildDepList, pkg.os, snapshot);
            if (result.length > 0) {
                errMsgs.push('- Invalid build dependency of ' + pkg.name + '(' + pkg.os +
                        '). The following package(s) must be in the distribution:' + result.join(','));
            }

            result = findSourceUnmetDependencies(pkg, snapshot);
            if (result.length > 0) {
                errMsgs.push('- Invalid source dependency of ' + pkg.name + '(' + pkg.os +
                        '). The following package(s) must be in the distribution:' + result.join(','));
            }
        }
    }

    if (errMsgs.length > 0) {
        return callback(new DError('TREPO016', {
            snapshot: snapshot.name,
            errMsg: errMsgs.join('\n')
        }));
    } else {
        return callback(null);
    }
}


// TODO: check version
function findUnmetDependencies(depList, pkgOs, snapshot) {
    var result = [];

    for (var i = 0; i < depList.length; i++) {
        var dep = depList[i];
        var os = (dep.os === undefined) ? pkgOs : dep.os;

        if (snapshot.osPackages[os] === undefined) {
            result.push('*:' + os);
        }
        var result2 = snapshot.osPackages[os][dep.packageName];
        if (result2 === undefined) {
            result.push(dep.packageName + ':' + os);
        }
    }

    return result;
}


function findSourceUnmetDependencies(pkg, snapshot) {
    var result = [];

    for (var i = 0; i < pkg.srcDepList.length; i++) {
        var dep = pkg.srcDepList[i];

        var result2 = snapshot.archivePackages.indexOf(dep.packageName);
        if (result2 === -1) {
            result.push(dep.packageName);
        }
    }

    return result;
}


function prepareToDownload(name, snapshot, distPath, os, callback) {

    async.waterfall([
        function (cb) {
            if (os) {
                if (snapshot.osPackages[os] === undefined) {
                    cb(new DError('TREPO012', {
                        os: os
                    }), null, null);
                    return;
                }

                var pkg = snapshot.osPackages[os][name];
                if (!pkg) {
                    cb(new DError('TREPO018', {
                        pkg: name,
                        os: os
                    }), null, null);
                } else {
                    if (Utils.isURL(distPath)) {
                        cb(null, distPath + '/' + pkg.path, pkg);
                    } else {
                        cb(null, path.join(distPath, pkg.path), pkg);
                    }
                }
            } else {
                var srcPkgs = snapshot.archivePackages.filter(function (e) {
                    return e === name;
                });
                if (srcPkgs.length === 0) {
                    cb(new DError('TREPO015', {
                        pkg: name
                    }), null, null);
                    return;
                } else {
                    if (Utils.isURL(distPath)) {
                        cb(null, distPath + '/source/' + name, srcPkgs[0]);
                    } else {
                        cb(null, path.join(distPath, 'source', name), srcPkgs[0]);
                    }
                }
            }
        },
        function (lpath, pkg, cb) {
            if (snapshot.remote) {
                cb(null, lpath);
            } else {
                var opts = {};
                if (pkg.path && pkg.size && pkg.checksum) {
                    opts.name = path.basename(pkg.path);
                    opts.size = pkg.size;
                    opts.checksum = pkg.checksum;
                }

                // NOTE. DOWNLOAD must be finished in 1 hour after adding this file to DFS
                //      Otherwise, added files will be removed on DFS
                opts.lifetime = 60 * 60 * 1000;
                dfs.addFile(null, lpath, opts, function (err, dfsPath2) {
                    cb(err, dfsPath2);
                });
            }
        }], function (err, dfsPath) {
        callback(err, dfsPath);
    });

}


function getInstallDependentPackages(pkgs, snapshot, opts, callback) {
    getInstallDependentPackagesInternal(pkgs, [], snapshot, function (err, resultPkgs) {
        if (!err) {
            for (var i = 0; i < pkgs.length; i++) {
                resultPkgs.splice(pkgs.indexOf(pkgs[i]), 1);
            }
            if (opts.installOrder === true) {
                Package.sortPackagesByInstallOrder(resultPkgs, [], callback);
            } else {
                callback(null, resultPkgs);
            }
        } else {
            callback(err, null);
        }
    });
}


function getInstallDependentPackagesInternal(srcPkgs, depPkgs, snapshot, callback) {
    if (srcPkgs.length === 0) {
        callback(null, depPkgs);
        return;
    }

    // add sources to depPkgs
    for (var i = 0; i < srcPkgs.length; i++) {
        if (depPkgs.indexOf(srcPkgs[i]) === -1) {
            depPkgs.push(srcPkgs[i]);
        }
    }

    var newSrcPkgs = [];
    for (var i = 0; i < srcPkgs.length; i++) {
        var pkg = srcPkgs[i];

        var depList = pkg.installDepList;
        for (var j = 0; j < depList.length; j++) {
            var dep = depList[j];
            var os = (dep.os === undefined) ? pkg.os : dep.os;

            if (snapshot.osPackages[os] === undefined) {
                callback(new DError('TREPO019', {
                    os: os
                }), null);
                return;
            }
            var depPkg = snapshot.osPackages[os][dep.packageName];
            if (depPkg === undefined) {
                callback(new DError('TREPO020', {
                    pkg: dep.packageName
                }), null);
                return;
            }

            if (depPkgs.indexOf(depPkg) === -1 && newSrcPkgs.indexOf(depPkg) === -1) {
                newSrcPkgs.push(depPkg);
            }
        }
    }

    // call with new src pkgs
    getInstallDependentPackagesInternal(newSrcPkgs, depPkgs, snapshot, callback);
}

function getInstallDependentPackagesWithExcludeList(pkgs, snapshot, excludeList, opts, callback) {
    getInstallDependentPackagesWithExcludeListInternal(pkgs, [], snapshot, excludeList, function (err, resultPkgs) {
        if (!err) {
            for (var i = 0; i < pkgs.length; i++) {
                resultPkgs.splice(pkgs.indexOf(pkgs[i]), 1);
            }
            if (opts.installOrder === true) {
                Package.sortPackagesByInstallOrder(resultPkgs, [], callback);
            } else {
                callback(null, resultPkgs);
            }
        } else {
            callback(err, null);
        }
    });
}

function getInstallDependentPackagesWithExcludeListInternal(srcPkgs, depPkgs, snapshot, excludeList, callback) {
    if (srcPkgs.length === 0) {
        callback(null, depPkgs);
        return;
    }

    // add sources to depPkgs
    for (var i = 0; i < srcPkgs.length; i++) {
        if (depPkgs.indexOf(srcPkgs[i]) === -1) {
            depPkgs.push(srcPkgs[i]);
        }
    }

    var newSrcPkgs = [];
    for (var i = 0; i < srcPkgs.length; i++) {
        var pkg = srcPkgs[i];

        var depList = pkg.installDepList;
        for (var j = 0; j < depList.length; j++) {
            var dep = depList[j];
            var os = (dep.os === undefined) ? pkg.os : dep.os;

            if (snapshot.osPackages[os] === undefined) {
                callback(new DError('TREPO019', {
                    os: os
                }), null);
                return;
            }
            var depPkg = snapshot.osPackages[os][dep.packageName];
            if (depPkg === undefined) {
                callback(new DError('TREPO020', {
                    pkg: dep.packageName
                }), null);
                return;
            }

            if (depPkgs.indexOf(depPkg) === -1 && newSrcPkgs.indexOf(depPkg) === -1) {
                newSrcPkgs.push(depPkg);
            }
        }
    }

    // call with new src pkgs
    getInstallDependentPackagesWithExcludeListInternal(_.difference(newSrcPkgs, excludeList), depPkgs, snapshot, excludeList, callback);
}

function loadRemoteSnapshots(distURL, repoOptions, callback) {
    var tempPath = null;
    async.waterfall([
        function (cb) {
            Utils.genTemp(cb);
        },
        function (tempDir, cb) {
            tempPath = tempDir;
            Utils.getTextFromUrl(distURL + '/snapshot.info', function (err, snapshotInfoString) {
                if (err) {
                    dibs.log.error('Failed to get snapshot.info from ' + distURL);
                    cb(err, null);
                } else {
                    cb(err, snapshotInfoString);
                }
            });
        },
        // load snapshot info
        function (snapshotInfoString, cb) {
            var snapshots = loadSnapshotInfoFromString(snapshotInfoString);
            if (repoOptions.snapshotName === null) {
                var latest = getLatestSnapshot(snapshots);
                if (latest) {
                    snapshots = [latest];
                } else {
                    snapshots = [];
                }
            } else if (repoOptions.snapshotName !== undefined) {
                snapshots = snapshots.filter(function (snapshot) {
                    return snapshot.name === repoOptions.snapshotName;
                });
            }
            var result = [];
            async.eachSeries(snapshots, function (snapshot, cb2) {
                var snapshotDirURL = distURL + '/' + snapshot.path;
                loadRemoteSnapshot(snapshotDirURL, function (err, s) {
                    if (!err) {
                        snapshot.archivePackages = s.archivePackages;
                        snapshot.osPackages = s.osPackages;
                        snapshot.remote = true;
                        result.push(snapshot);
                        cb2(null);
                    } else {
                        // NOTE. MUST controll whether ignore invalid snapshots or not
                        cb2(null);
                    }
                });
            }, function (err) {
                cb(err, result);
            });
        }], function (err, snapshots) {
        if (tempPath !== null) {
            extfs.remove(tempPath, function () {
                callback(err, snapshots);
            });
        } else {
            callback(err, snapshots);
        }
    });
}


function getLatestSnapshot(snapshots) {
    var result = null;

    // choose the latest 'auto' snapshot
    for (var i = 0; i < snapshots.length; i++) {
        if (snapshots[i].type === 'auto') {
            result = snapshots[i];
        }
    }
    // if not, choose latest snapshot
    if (result === null && snapshots.length > 0) {
        result = snapshots[snapshots.length - 1];
    }

    return result;
}


function loadRemoteSnapshot(snapshotDirURL, callback) {
    var newObj = new TizenSnapshot('Empty');

    async.waterfall([
        function (cb) {
            Utils.getTextFromUrl(snapshotDirURL + '/archive_pkg_list', function (err, contents) {
                if (err) {
                    cb(null, null);
                } else {
                    cb(null, contents);
                }
            });
        },
        function (archivePkgString, cb) {
            if (archivePkgString) {
                newObj.archivePackages = loadArchivePackages(archivePkgString);
            } else {
                newObj.archivePackages = [];
            }
            loadOsPackagesFromUrl(snapshotDirURL, cb);
        },
        function (packages, cb) {
            newObj.osPackages = packages;
            cb(null, newObj);
        }], callback);
}


function getUpdatedPackages(newSnapshot, oldSnapshot, options) {
    // 'options' is optional argument
    if (!options) {
        options = {};
    }

    // if no new snapshot, return emtpy
    if (!newSnapshot) {
        return [];
    }

    // check archive packages
    var pkgs1 = [];
    if (newSnapshot && newSnapshot.archivePackages) {
        pkgs1 = newSnapshot.archivePackages.filter(function (e) {
            if (oldSnapshot === null) {
                return true;
            } else {
                return (oldSnapshot.archivePackages.indexOf(e) === -1);
            }
        });
    }

    // check os packages
    var pkgs2 = [];
    if (newSnapshot && newSnapshot.osPackages) {
        for (var os in newSnapshot.osPackages) {
            // skip when os is in 'osList' option
            if (options.osList && options.osList.indexOf(os) < 0) {
                continue;
            }

            var packages = newSnapshot.osPackages[os];
            for (var pname in packages) {
                var newPkg = _.clone(packages[pname]);
                // NOTE. This is the guard code for preventing confused compatible OS name
                if (newPkg.os !== os) {
                    newPkg.os = os;
                }

                var oldPackages = (oldSnapshot !== null) ? oldSnapshot.osPackages : null;
                if (oldPackages === null || oldPackages[os] === undefined) {
                    pkgs2.push(newPkg);
                } else {
                    var oldPkg = oldPackages[os][pname];
                    if (oldPkg === undefined) {
                        pkgs2.push(newPkg);
                    } else {
                        if (Package.compareVersion(newPkg.version, oldPkg.version) > 0) {
                            pkgs2.push(newPkg);
                        } else if (options.SYNC_FORCE && Package.compareVersion(newPkg.version, oldPkg.version) === 0) {
                            if (newPkg.size !== oldPkg.size && newPkg.checksum !== oldPkg.checksum) {
                                pkgs2.push(newPkg);
                            }
                        } else {

                        }
                    }
                }
            }
        }
    }

    return pkgs1.concat(pkgs2);
}


// CONFLICT = same version and different package info
function getConflictPackages(newSnapshot, oldSnapshot, options) {
    // 'options' is optional argument
    if (!options) {
        options = {};
    }

    // if no new snapshot, return emtpy
    if (!newSnapshot) {
        return [];
    }

    // check os packages
    var pkgs2 = [];
    if (newSnapshot && newSnapshot.osPackages) {
        for (var os in newSnapshot.osPackages) {
            // skip when os is in 'osList' option
            if (options.osList && options.osList.indexOf(os) < 0) {
                continue;
            }

            var packages = newSnapshot.osPackages[os];
            for (var pname in packages) {
                var newPkg = _.clone(packages[pname]);
                // NOTE. This is the guard code for preventing confused compatible OS name
                if (newPkg.os !== os) {
                    newPkg.os = os;
                }

                var oldPackages = (oldSnapshot !== null) ? oldSnapshot.osPackages : null;
                if (oldPackages && oldPackages[os] && oldPackages[os][pname]) {
                    var oldPkg = oldPackages[os][pname];
                    if (Package.compareVersion(newPkg.version, oldPkg.version) === 0 &&
                        (newPkg.size !== oldPkg.size ||
                        newPkg.checksum !== oldPkg.checksum)) {
                        pkgs2.push(newPkg);
                    }
                }
            }
        }
    }

    return pkgs2;
}


function getOutdatedPackages(newSnapshot, oldSnapshot, options) {
    // 'options' is optional argument
    if (!options) {
        options = {};
    }

    // if no new snapshot, return emtpy
    if (!newSnapshot) {
        return [];
    }

    // check os packages
    var pkgs2 = [];
    if (newSnapshot && newSnapshot.osPackages) {
        for (var os in newSnapshot.osPackages) {
            // skip when os is in 'osList' option
            if (options.osList && options.osList.indexOf(os) < 0) {
                continue;
            }

            var packages = newSnapshot.osPackages[os];
            for (var pname in packages) {
                var newPkg = _.clone(packages[pname]);
                // NOTE. This is the guard code for preventing confused compatible OS name
                if (newPkg.os !== os) {
                    newPkg.os = os;
                }

                var oldPackages = (oldSnapshot !== null) ? oldSnapshot.osPackages : null;
                if (oldPackages && oldPackages[os] && oldPackages[os][pname]) {
                    var oldPkg = oldPackages[os][pname];
                    if (Package.compareVersion(newPkg.version, oldPkg.version) < 0) {
                        pkgs2.push(newPkg);
                    }
                }
            }
        }
    }

    return pkgs2;
}


function getPackage(snapshot, name, os) {
    if (snapshot && snapshot.osPackages && snapshot.osPackages[os] &&
        snapshot.osPackages[os][name]) {

        return snapshot.osPackages[os][name];
    } else {
        return null;
    }
}


function checkChangeLog(changelog) {
    if (!changelog) {
        return {
            err: null
        };
    }

    if (Utils.includesSpecialChars(changelog.contents)) {
        return {
            err: new DError('TREPO025', {
                msg: changelog.map(function (e) {
                    return e.contents;
                }).join('--->\n')
            })
        };
    } else {
        return {
            err: null
        };
    }
}

function updateRefCountHash(hash, snapshot) {
    _.each(snapshot.archivePackages,
        function (apkg) {
            var apkgPath = path.sep + path.join('source', apkg);
            if (hash[apkgPath]) {
                hash[apkgPath] += 1;
            } else {
                hash[apkgPath] = 1;
            }
        });
    _.each(snapshot.osPackages,
        function (os) {
            _.each(os, function (pkg) {
                if (hash[pkg.path]) {
                    hash[pkg.path] += 1;
                } else {
                    hash[pkg.path] = 1;
                }
            });
        });
    return hash;
}


function getPkgFileRefCountHash(distPath, snapshotInfoList, callback) {
    async.reduce(snapshotInfoList, {},
        function (pkgFileRefCountHash, fsnapshot, rcb) {
            loadSnapshot(path.join(distPath, fsnapshot.path),
                function (err, fsnapshot) {
                    if (!err) {
                        updateRefCountHash(pkgFileRefCountHash, fsnapshot);
                    }
                    rcb(err, pkgFileRefCountHash);
                });
        }, callback);
}


function checkUnmetDepsIfUpdated(snapshot, updatedPkgs) {
    // gather all deps
    var deps = Package.getAllDepsOfPackages(updatedPkgs);

    var unmetDeps = [];
    deps.forEach(function (dep) {

        // check depedency in updated pkgs  first
        var matched = updatedPkgs.filter(function (pkg) {
            if (pkg.name !== dep.name) {
                return false;
            }
            if (dep.os && pkg.os !== dep.os) {
                return false;
            }
            return true;
        });
        if (matched.length > 0) {
            return;
        }

        // check dependency in snapshot
        if (!snapshot) {
            unmetDeps.push(dep);
        }
        if (dep.os) {
            if (snapshot && (!snapshot.osPackages ||
                !snapshot.osPackages[dep.os] ||
                !snapshot.osPackages[dep.os][dep.name])) {

                // check archive-package
                if (!snapshot.archivePackages || (snapshot.archivePackages.indexOf[dep.name] === -1)) {
                    unmetDeps.push(dep);
                }
            }
        } else {
            if (!snapshot.archivePackages || (snapshot.archivePackages.indexOf[dep.name] === -1)) {
                unmetDeps.push(dep);
            }
        }
    });

    return unmetDeps;
}

module.exports.getFilteredSnapshot = getFilteredSnapshot;
// include || exclude must be meta package => (attr : root)
function getFilteredSnapshot(snapshot, include, exclude, useExcludeOnly, useInstallDep, useBuildDep) {
    var metaList = [];
    if (useExcludeOnly) {
        var excludeMetaPkgs = getMetaPkgs(pkgNameToPkgs(snapshot, exclude));
        var excludeDepPkgs = getDepTrace(snapshot, excludeMetaPkgs, [], true, false, true, false);
        var excludeDepMeta = _.union(excludeMetaPkgs, getMetaPkgs(excludeDepPkgs));
        var allMeta = getAllMetas(snapshot);
        var extraIncludeList = [];
        _.each(allMeta, function (meta) {
            if (_.indexOf(excludeDepMeta, meta) < 0) {
                _.each(getMetaPkgs(depToPkgs(snapshot, meta.os, meta.installDepList)), function (metaDepPkg) {
                    if (_.indexOf(excludeDepMeta, metaDepPkg) > 0) {
                        extraIncludeList.push(metaDepPkg);
                    }
                });
            }
        });
        extraIncludeList = getDepTrace(snapshot, extraIncludeList, exclude, true, false, true, false);

        var realExcludes = _.difference(excludeDepMeta, extraIncludeList);
        metaList = _.difference(allMeta, realExcludes);
    } else {
        var includeMetaPkgs = getMetaPkgs(pkgNameToPkgs(snapshot, include));
        metaList = getDepTrace(snapshot, includeMetaPkgs, exclude, true, false, true, false);
    }

    return getSnapshotFromSpecificMetaPackages(metaList, snapshot, useInstallDep, useBuildDep);
}


function getSnapshotFromSpecificMetaPackages(specificMetaList, snapshot, useInstallDep, useBuildDep) {
    var newSnapshot = _.clone(snapshot);
    newSnapshot.osPackages = {};
    newSnapshot.archivePackages = [];
    var archivePackages = [];

    var pkgDepList = getDepTrace(snapshot, specificMetaList, [], useInstallDep, useBuildDep, false, true);
    _.each(pkgDepList, function (pkg) {
        if (pkg.os === 'archive') {
            archivePackages.push(pkg.name);
        } else {
            if (!newSnapshot.osPackages[pkg.os]) {
                newSnapshot.osPackages[pkg.os] = {};
            }
            newSnapshot.osPackages[pkg.os][pkg.name] = pkg;
        }
    });
    newSnapshot.archivePackages = _.uniq(archivePackages);

    return newSnapshot;
}


function getDepTrace(snapshot, pkgs, excludeNameList, useInstallDep, useBuildDep, onlyMeta, withoutMeta) {
    var uniq = {};
    var depPkgs = _.flatten(_.map(pkgs, function (dpkg) {
        if (_.indexOf(excludeNameList, dpkg.name) < 0) {
            return getDepTraceT(snapshot, dpkg, excludeNameList, useInstallDep, useBuildDep, onlyMeta, withoutMeta);
        } else {
            return [];
        }
    }), true);

    _.each(depPkgs, function (dp) {
        uniq[dp.name + ':' + dp.os] = dp;
    });
    return _.values(uniq);
}


function getDepTraceT(snapshot, pkg, excludeNameList, useInstallDep, useBuildDep, onlyMeta, withoutMeta) {
    var depPkgs = [];
    if (useInstallDep) {
        depPkgs = _.union(depPkgs, depToPkgs(snapshot, pkg.os, pkg.installDepList));
    }

    if (useBuildDep) {
        depPkgs = _.union(depPkgs, depToPkgs(snapshot, pkg.os, pkg.buildDepList));
        depPkgs = _.union(depPkgs, depToPkgs(snapshot, pkg.os, pkg.srcDepList));
    }

    if (excludeNameList && excludeNameList.length > 0) {
        depPkgs = _.filter(depPkgs, function (dpkg) {
            return (_.indexOf(excludeNameList, dpkg.name) < 0);
        });
    }

    if (onlyMeta) {
        depPkgs = getMetaPkgs(depPkgs);
    }

    if (withoutMeta) {
        depPkgs = getWithoutMetaPkgs(depPkgs);
    }

    if (depPkgs.length > 0) {
        var children = _.flatten(_.map(depPkgs, function (dpkg) {
            return getDepTraceT(snapshot, dpkg, excludeNameList, useInstallDep, useBuildDep, onlyMeta, withoutMeta);
        }), true);
        children.push(pkg);
        return children;
    } else {
        return [pkg];
    }
}

function disp(list) {
    return _.map(list, function (item) {
        return item.name + ':' + item.os;
    });
}

function getAllMetas(snapshot) {
    var allMetas = [];
    _.each(snapshot.osPackages, function (osPkgs) {
        allMetas = _.union(allMetas, getMetaPkgs(_.values(osPkgs)));
    });
    return allMetas;
}

function getMetaPkgs(pkgs) {
    return _.filter(pkgs, function (pkg) {
        return pkg.attr === 'root';
    });
}

function getWithoutMetaPkgs(pkgs) {
    return _.filter(pkgs, function (pkg) {
        return pkg.attr !== 'root';
    });
}

function pkgNameToPkgs(snapshot, pkgNames) {
    var pkgs = [];
    _.each(snapshot.osPackages, function (osPkgs) {
        _.each(pkgNames, function (pkgName) {
            if (osPkgs[pkgName]) {
                pkgs.push(osPkgs[pkgName]);
            }
        });
    });
    return pkgs;
}

function depToPkgs(snapshot, currentOS, deps) {
    var pkgs = [];
    _.each(deps, function (dep) {
        if (dep.os && snapshot.osPackages[dep.os][dep.packageName]) {
            pkgs.push(snapshot.osPackages[dep.os][dep.packageName]);
        } else if (snapshot.osPackages[currentOS][dep.packageName]) {
            pkgs.push(snapshot.osPackages[currentOS][dep.packageName]);
        } else if (_.indexOf(snapshot.archivePackages, dep.packageName) > -1) {
            pkgs.push({
                name: dep.packageName,
                os: 'archive'
            });
        }
    });
    return pkgs;
}
