/**
 * 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');
var SnapshotModel = require('../dibs.model.common/snapshot.js');

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

module.exports.searchSnapshots = searchSnapshots;
module.exports.searchSnapshotList = searchSnapshotList;

module.exports.searchBinaries = searchBinaryArtifacts;
module.exports.searchArchives = searchArchiveArtifacts;
module.exports.removeAritifactWithDB = removeArtifactWithDB;

module.exports.registerPackages = registerPackages;
module.exports.deregisterPackages = deregisterPackages;
module.exports.generateSnapshot = generateSnapshot;


function TizenSnapshot(name) {
    var self = this;
    this.name = name;
    this.type = 'auto';
    this.time = null;
    this.path = null;
    this.changes = 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) {
                if (err) {
                    return callback(new DError('TREPO035', err), newObj);
                } else {
                    return callback(null, 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 createSnapshotWithDB(distribution, baseSnapshotName, opts, callback) {
    var timestamp = Utils.generateTimeStamp(true);

    var name;
    var path;
    var attribute;
    var uploader = 'DIBS';

    var distName = distribution.name;

    if (opts && opts.syncSnapshotName) {
        if (opts.syncPrefix) {
            name = opts.syncPrefix + opts.syncSnapshotName;
        } else {
            name = opts.syncSnapshotName;
        }
    } else if (opts && opts.isTemp) {
        name = 'TEMP_JOB' + opts.tempJobId;
    } else if (opts && opts.name) {
        name = opts.name;
    } else {
        name = timestamp;
    }

    if (opts && opts.snapshotType === 'manual') {
        attribute = opts.snapshotType;
    } else if (opts && opts.isTemp) {
        attribute = 'temp';
    } else {
        attribute = 'auto';
    }

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

    if (opts && opts.isTemp) {
        path = '/temp/jobs/' + opts.tempJobId + '/';
    } else if (opts && opts.path) {
        path = opts.path;
    } else {
        path = '/snapshots/' + name;
    }

    async.waterfall([
        function (cb) {
            var cond = {
                distName: distName,
                name: name
            };

            dibs.rpc.datamgr.searchSnapshots(cond, function (err, results) {
                if (err) {
                    return cb(err);
                } else if (results && results.length > 0) {
                    if (opts.force === undefined || opts.force === false) {
                        return cb(new DError('TREPO022', {snapshot: name}));
                    } else {
                        var snapshotList = [];
                        snapshotList.push(name);
                        removeSnapshots(snapshotList, distribution, opts, function (err) {
                            cb(err);
                        });
                    }
                } else {
                    return cb(null);
                }
            });
        },
        function (cb) {
            if (!baseSnapshotName) {
                var options = {};

                options['__ENVIRONMENTS'] = [];
                _.each(dibs.plugin.getExtensions('dibs.base.environment'), function (env) {
                    if (env.attributes.type === 'tizen') {
                        options['__ENVIRONMENTS'].push(env.attributes.name);
                    }
                });

                options.changes = '';

                SnapshotModel.create(name, distName, 'tizen', timestamp, 'OPEN', attribute, path, opts.desc, options,
                    function (err, s) {
                        s.artifacts = [];
                        s.options.uploader = uploader;
                        cb(err, s, null);
                    });
            } else {
                var cond = {
                    distName: distName,
                    name: baseSnapshotName,
                    artifacts: true
                };
                dibs.rpc.datamgr.searchSnapshots(cond, function (err, results) {
                    if (err) {
                        return cb(err);
                    } else if (results.length !== 1) {
                        return cb(new DError('TREPO023', {ref: baseSnapshotName}));
                    } else {
                        var oldSnapshotData = results[0];
                        var newSnapshotData = _.clone(oldSnapshotData);
                        newSnapshotData.name = name;
                        newSnapshotData.path = path;
                        newSnapshotData.time = timestamp;
                        newSnapshotData.attribute = attribute;
                        newSnapshotData.options.uploader = uploader;

                        return cb(null, newSnapshotData, oldSnapshotData);
                    }
                });
            }
        }], callback);
}

function removeSnapshots(snapshotList, distribution, opts, callback) {
    var removedArtifacts = [];
    async.series([
        function (cb) {
            async.eachSeries(snapshotList, function (snapshotName, cb1) {
                dibs.rpc.datamgr.searchSnapshots({
                    distName: distribution.name,
                    name: snapshotName
                }, function (err1, results) {
                    if (err1) {
                        return cb1(err1);
                    } else if (!results || results.length === 0) {
                        return cb1(null);
                    } else {
                        dibs.rpc.datamgr.deleteSnapshot(results[0].id, function (err1, artifacts) {
                            if (_.isArray(artifacts)) {
                                removedArtifacts = _.union(removedArtifacts, artifacts);
                            }
                            cb1(err1);
                        });
                    }
                });
            }, cb);
        },
        function (cb) {
            removeSnapshotsFile(snapshotList, distribution.path, cb);
        },
        function (cb) {
            async.eachSeries(removedArtifacts, function (artifact, cb1) {
                dibs.rpc.datamgr.searchArtifacts({distName: artifact.distName, path: artifact.path, status: 'OPEN'}, function (err1, results) {
                    if (err1) {
                        return cb1(err1);
                    } else if (results && results.length > 0) {
                        // skip remove file
                        return cb1(null);
                    } else {
                        var file = path.join(distribution.path, artifact.path);
                        extfs.remove(file, function (err1) {
                            if (err1) {
                                dibs.log.warn('Fail remove package files... ' + file);
                            }
                            cb1(err1);
                        });
                    }
                });
            }, cb);
        }
    ], callback);
}

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

    async.series([
        function (scb) {
            // create directory
            extfs.mkdirs(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 (snapshot.type !== 'temp' && snapshots.indexOf(snapshot) === snapshots.length - 1) {
                copySnapshotFilesToRoot(snapshot, distPath, scb);
            } else {
                return scb(null);
            }
        }
    ], function (err) {
        if (err) {
            return callback(new DError('TREPO034', err));
        } else {
            return callback(err);
        }
    });
}


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

    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) {
            fs.exists(path.join(snapshotDirPath, 'archive_pkg_list'), function (exists) {
                if (!exists) {
                    return cb(null);
                }
                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) {
                fs.exists(path.join(snapshotDirPath, 'pkg_list_' + os), function (exists) {
                    if (!exists) {
                        return cb1(null);
                    }
                    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 getChangeLogs(snapshot) {
    snapshot.changes = '';
    // date , uploader
    var dateFormat = Utils.getTimeString(Utils.getDateFromTimeStamp(snapshot.time));
    if (snapshot.uploader) {
        snapshot.changes += dateFormat + ' <' + snapshot.uploader + '>\n';
    } else {
        snapshot.changes += dateFormat + ' <' + 'admin@user' + '>\n';
    }
    // new pkgs
    if (snapshot.newPkgs && snapshot.newPkgs.length > 0) {
        appendNewPackagesToChangeLog(snapshot);
    }
    // removed pkgs
    if (snapshot.removedPkgs && snapshot.removedPkgs.length > 0) {
        appendRemovedPackagesToChangeLog(snapshot);
    }
    // update pkgs
    if (snapshot.updatedPkgs && snapshot.updatedPkgs.length > 0) {
        appendUpdatedPackagesToChangeLog(snapshot);
    }

    return;
}

function saveChangeLogs(snapshot, changeLogDirPath, callback) {
    // write to file
    fs.writeFile(path.join(changeLogDirPath, snapshot.name + '.log'), snapshot.changes,
        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.changes += '* New Packages\n';
    for (var pkgName in pkgs) {
        if (pkgs[pkgName].version) {
            snapshot.changes += ' - ' + pkgName + ' (' + pkgs[pkgName].version + ') [' + pkgs[pkgName].osList.join(',') + ']\n';
            snapshot.changes += pkgs[pkgName].changeLog + '\n';
        } else {
            snapshot.changes += ' - ' + 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.changes += '* Removed Packages\n';
    for (var pkgName in pkgs) {
        if (pkgs[pkgName].version) {
            snapshot.changes += ' - ' + pkgName + ' (' + pkgs[pkgName].version + ') [' + pkgs[pkgName].osList.join(',') + ']\n';
        } else {
            snapshot.changes += ' - ' + 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.changes += '* Updated Packages\n';
    for (var pkgName in pkgs) {
        if (pkgs[pkgName].version) {
            snapshot.changes += ' - ' + pkgName + ' (' + pkgs[pkgName].version + ') [' + pkgs[pkgName].osList.join(',') + ']\n';
            snapshot.changes += pkgs[pkgName].changeLog + '\n';
        } else {
            snapshot.changes += ' - ' + 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 removeSnapshotsFile(snapshotNameList, distPath, callback) {
    var removeSnapshotInfoList = [];
    var removedSnapshotInfoList = [];

    var loadDistSnapshotInfo = function (cb) {
        loadSnapshotInfo(distPath, cb);
    };
    var saveRemovedSnapshotInfo = function (snapshotInfoList, cb) {
        removeSnapshotInfoList = selectSnapshotInfo(snapshotNameList, snapshotInfoList);
        removedSnapshotInfoList = removeSnapshotInfo(snapshotNameList, snapshotInfoList);
        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,
        saveRemovedSnapshotInfo,
        removeDirs
    ],
        function (err) {
            callback(err);
        });
}


function registerPackages(fileInfos, distribution, opts, callback) {
    var distName = distribution.name;
    var latestSnapshotName;
    if (distribution.latestSnapshot) {
        latestSnapshotName = distribution.latestSnapshot.name;
    }

    async.waterfall([
        function (cb) {
            createSnapshotWithDB(distribution, latestSnapshotName, opts, cb);
        },
        function (newSnapshotData, oldSnapshotData, cb) {
            newSnapshotData.updatedPkgs = [];
            newSnapshotData.newPkgs = [];
            newSnapshotData.removedPkgs = [];
            newSnapshotData.overwritePkgs = [];

            // append package to new snapshot
            async.each(_.keys(fileInfos), function (dfsPath, cb1) {
                var pkg = fileInfos[dfsPath];

                if (pkg !== null) {
                    appendBinaryPackageToSnapshot(distName, oldSnapshotData, newSnapshotData, pkg, dfsPath, opts, cb1);
                } else {
                    appendArchivePackageToSnapshot(distName, oldSnapshotData, newSnapshotData, dfsPath, cb1);
                }
            }, function (err) {
                cb(err, newSnapshotData);
            });
        },
        function (newSnapshotData, cb) {
            saveWithDB(distribution, fileInfos, newSnapshotData, opts, cb);
        },
        function (newSnapshot, cb) {
            // validate
            if (!opts.skipIntegrityCheck) {
                validateSnapshot(newSnapshot, function (err) {
                    cb(err, newSnapshot);
                });
            } else {
                return cb(null, newSnapshot);
            }
        }
    ], callback);
}

function generateSnapshot(distribution, name, opts, callback) {
    var baseSnapshotName;

    opts.name = name;

    if (opts.refSnapshot) {
        baseSnapshotName = opts.refSnapshot;
    } else {
        baseSnapshotName = distribution.latestSnapshot.name;
    }

    async.waterfall([
        function (cb) {
            createSnapshotWithDB(distribution, baseSnapshotName, opts, cb);
        },
        function (newSnapshotData, oldSnapshotData, cb) {
            saveWithDB(distribution, [], newSnapshotData, opts, cb);
        }
    ], function (err, newSnapshot) {
        callback(err, newSnapshot);
    });
}

function saveWithDB(distribution, fileInfos, newSnapshotData, opts, callback) {
    var conditions = {
        distName: distribution.name,
        name: newSnapshotData.name
    };

    var newSnapshot = convertRawToTizenSnapshot(newSnapshotData);
    getChangeLogs(newSnapshot);

    newSnapshotData.options.changes = newSnapshot.changes;

    async.series([
        function (cb) {
            dibs.rpc.datamgr.searchSnapshots(conditions, function (err, results) {
                if (err) {
                    return cb(err);
                } else if (results && results.length > 0) {
                    return cb(new DError('TREPO022', { snapshot: newSnapshotData.name}));
                } else {
                    return cb(null);
                }
            });
        },
        function (cb) {
            if (_.isEmpty(fileInfos)) {
                return cb(null);
            } else {
                copyFilesToRepository(fileInfos, opts, distribution.path, cb);
            }
        },
        function (cb) {
            async.eachSeries(newSnapshotData.overwritePkgs, function (artifact, cb1) {
                dibs.rpc.datamgr.updateArtifact(artifact, cb1);
            }, function (err) {
                if (err) {
                    return cb(new DError('TREPO039', err));
                } else {
                    return cb(null);
                }
            });
        },
        function (cb) {
            newSnapshotData.options['ARTIFACTS'] = newSnapshotData.artifacts;
            dibs.rpc.datamgr.addSnapshot(newSnapshotData.name,
                distribution.name,
                newSnapshotData.type,
                newSnapshotData.time,
                'OPEN',
                newSnapshotData.attribute,
                newSnapshotData.path,
                newSnapshotData.desc,
                newSnapshotData.options,
                function (err) {
                    if (err) {
                        return cb(new DError('TREPO038', err));
                    } else {
                        return cb(null);
                    }
                });
        },
        function (cb) {

            saveSnapshot(newSnapshot, distribution.path, cb);
        }
    ], function (err) {
        callback(err, newSnapshot);
    });
}


function copyFilesToRepository(fileInfos, opts, distPath, callback) {
    var copied = [];

    async.eachSeries(Object.keys(fileInfos),
        function (dfsPath, cb) {
            var pkg = fileInfos[dfsPath];

            if (pkg && opts.isTemp) {
                copyTempFileToRepository(dfsPath, distPath, opts,
                    function (err, file) {
                        if (!err) {
                            copied.push(file);
                        }
                        cb(err);
                    });
            } else if (pkg) { // binary package
                copyBinaryPackageFilesToRepository(dfsPath, pkg, distPath, opts,
                    function (err, files) {
                        if (!err) {
                            copied = copied.concat(files);
                        }
                        cb(err);
                    });
            } else {
                // archive package
                copyArchivePackageFileToRepository(dfsPath, distPath, opts,
                    function (err, file) {
                        if (!err) {
                            copied.push(file);
                        }
                        cb(err);
                    });

            }
        },
        function (err) {
            if (err) {
                return callback(new DError('TREPO037', err), copied);
            } else {
                return callback(null, copied);
            }
        });
}


function copyBinaryPackageFilesToRepository(dfsPath, pkg, distPath, opts, callback) {
    if (opts.uploadCompat) {
        var copied = [];
        async.eachSeries(pkg.osList,
            function (os, cb) {
                copyOsBinaryPackageFileToRepository(dfsPath, pkg, os, distPath, opts,
                    function (err, filePath) {
                        if (!err) {
                            copied.push(filePath);
                        }
                        cb(err);
                    });
            }, function (err) {
                callback(err, copied);
            });
    } else {
        copyOsBinaryPackageFileToRepository(dfsPath, pkg, pkg.os, distPath, opts,
            function (err, filePath) {
                callback(err, [filePath]);
            });
    }
}


function copyOsBinaryPackageFileToRepository(dfsPath, pkg, os, distPath, opts, callback) {
    var tpath = path.join(distPath, 'binary',
        TizenUtils.getPackageFileNameFromInfo(pkg.name, pkg.version, os));
    // get file from remote
    dfs.getFile(tpath, dfsPath, function (err) {
        callback(err, tpath);
    });
}


function copyArchivePackageFileToRepository(dfsPath, distPath, opts, callback) {
    var tpath = path.join(distPath, 'source', path.basename(dfsPath));
    // get file from remote
    dfs.getFile(tpath, dfsPath, function (err) {
        callback(err, tpath);
    });
}


function copyTempFileToRepository(dfsPath, distPath, opts, callback) {
    var tpath = path.join(distPath, 'temp', 'jobs', opts.tempJobId.toString(), path.basename(dfsPath));
    // get file from remote
    dfs.getFile(tpath, dfsPath, function (err) {
        callback(err, tpath);
    });
}


function appendBinaryPackageToSnapshot(distName, oldSnapshotData, newSnapshotData, pkg, dfsPath, opts, callback) {
    var PATH_PREFIX;
    if (opts && opts.PATH_PREFIX) {
        PATH_PREFIX = opts.PATH_PREFIX;
        pkg.status = 'OPEN';
    } else if (opts && opts.isTemp) {
        PATH_PREFIX = '/temp/jobs/' + opts.tempJobId + '/';
        pkg.status = 'TEMP';
    } else {
        PATH_PREFIX = '/binary/';
        pkg.status = 'OPEN';
    }

    // 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) {
        return callback(new DError('TREPO012', {
            os: pkg.os
        }));
    }

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

    var oldPkgData = null;
    var newPkgData = null;

    if (opts.uploadCompat) {
        _.each(pkg.osList, function (os) {
            // skip if there is no os environment
            if (osList.indexOf(os) === -1) {
                return;
            }

            if (oldSnapshotData) {
                oldPkgData = _.findWhere(oldSnapshotData, {
                    name: pkg.name,
                    environment: pkg.os
                });

                var err = comparePackage(oldPkgData, pkg);
                if (err) {
                    if (opts.forceUpload && err.errno === 'TREPO024') {
                        oldPkgData.status = 'CLOSE';
                        newSnapshotData.overwritePkgs.push(oldPkgData);
                    } else {
                        return callback(err);
                    }
                }
            }

            var tempPkg = _.clone(pkg);
            tempPkg.os = os;
            tempPkg.path = PATH_PREFIX + TizenUtils.getPackageFileNameFromInfo(pkg.name, pkg.version, os);

            newPkgData = convertTizenPackageToRaw(tempPkg, distName);

            // add new or updated pkgs
            if (oldPkgData) {
                newSnapshotData.updatedPkgs.push(tempPkg);
            } else {
                newSnapshotData.newPkgs.push(tempPkg);
            }

            // set
            newPkgData.options['__SNAPSHOT_NAME'] = newSnapshotData.name;
            newSnapshotData.artifacts.push(newPkgData);
        });
    } else {
        pkg.path = PATH_PREFIX + TizenUtils.getPackageFileNameFromInfo(pkg.name, pkg.version, pkg.os);
        newPkgData = convertTizenPackageToRaw(pkg, distName);

        // check version
        if (!opts.forceUpload && oldSnapshotData && oldSnapshotData.artifacts && !opts.isTemp) {
            oldPkgData = _.findWhere(oldSnapshotData.artifacts, {
                name: pkg.name,
                environment: pkg.os
            });

            var err = comparePackage(oldPkgData, pkg);
            if (err) {
                if (opts.forceUpload && err.errno === 'TREPO024') {
                    oldPkgData.status = 'CLOSE';
                    newSnapshotData.overwritePkgs.push(oldPkgData);
                } else {
                    return callback(err);
                }
            }
        }

        // add new or updated pkgs
        if (oldPkgData) {
            newSnapshotData.updatedPkgs.push(pkg);
        } else {
            newSnapshotData.newPkgs.push(pkg);
        }

        // set
        newSnapshotData.artifacts = _.reject(newSnapshotData.artifacts, function (artifact) {
            return artifact.name === newPkgData.name && artifact.environment === newPkgData.environment;
        });
        newPkgData.options['__SNAPSHOT_NAME'] = newSnapshotData.name;
        newSnapshotData.artifacts.push(newPkgData);
    }
    callback(null);
}

function comparePackage(oldPkg, newPkg) {
    if (!oldPkg) {
        return null;
    }

    if (Package.compareVersion(newPkg.version, oldPkg.version) <= 0) {
        return new DError('TREPO013', {
            new: newPkg.version,
            old: oldPkg.version
        });
    }

    if (Package.compareVersion(newPkg.version, oldPkg.version) === 0 &&
        (newPkg.size !== oldPkg.size || newPkg.checksum !== oldPkg.checksum)) {
        return new DError('TREPO024', {
            pkgName: newPkg.name
        });
    }
}

function appendArchivePackageToSnapshot(distName, oldSnapshotData, newSnapshotData, dfsPath, callback) {
    var filename = path.basename(dfsPath);

    var oldPkgData = null;
    if (oldSnapshotData) {
        oldPkgData = _.findWhere(oldSnapshotData.artifacts, {
            name: filename
        });
    }

    var newPkgData = {
        name: filename,
        distName: distName,
        type: 'archive',
        version: null,
        environment: null,
        attribute: null,
        file: filename,
        path: '/source/' + filename,
        size: 0,
        checksum: null,
        description: null,
        options: {}
    };

    if (!oldPkgData) {
        // add new
        newSnapshotData.newPkgs.push(filename);
        // set
        newSnapshotData.artifacts.push(newPkgData);
    } else {
        // no-update
    }
    // set
    newPkgData.options['__SNAPSHOT_NAME'] = newSnapshotData.name;
    callback(null);
}


function deregisterPackages(names, distribution, opts, callback) {
    var latestSnapshotName;

    if (!distribution.latestSnapshot) {
        return callback(new DError('TREPO033'), null);
    } else {
        latestSnapshotName = distribution.latestSnapshot.name;
    }

    async.waterfall([
        function (cb) {
            createSnapshotWithDB(distribution, latestSnapshotName, opts, cb);
        },
        function (newSnapshotData, oldSnapshotData, cb) {
            var removeList = [];
            newSnapshotData.removedPkgs = [];

            newSnapshotData.artifacts = _.reject(newSnapshotData.artifacts, function (artifact) {
                var contain = _.contains(names, artifact.name);
                if (contain) {
                    if (artifact.type === 'binary' && (!opts.os || opts.os === artifact.environment)) {
                        removeList.push(artifact.name);
                        newSnapshotData.removedPkgs.push(convertRawToTizenPackage(artifact));
                        return true;
                    } else if (artifact.type === 'archive') {
                        removeList.push(artifact.name);
                        newSnapshotData.removedPkgs.push(artifact.name);
                        return true;
                    } else {
                        return false;
                    }
                } else {
                    return false;
                }
            });

            if (!_.isEmpty(removeList)) {
                saveWithDB(distribution, [], newSnapshotData, opts, cb);
            } else {
                return cb(new DError('TREPO014', {
                    pkg: removeList.toString(),
                    os: 'any'
                }), null);
            }
        },
        function (newSnapshot, cb) {
            // validate
            if (!opts.skipIntegrityCheck) {
                validateSnapshot(newSnapshot, function (err) {
                    cb(err, newSnapshot);
                });
            } else {
                return cb(null, newSnapshot);
            }
        }
    ], callback);
}

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) {
                    return cb(new DError('TREPO018', {
                        pkg: name,
                        os: os
                    }), null, null);
                } else {
                    if (Utils.isURL(distPath)) {
                        return cb(null, distPath + '/' + pkg.path, pkg);
                    } else {
                        return 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)) {
                        return cb(null, distPath + '/source/' + name, srcPkgs[0]);
                    } else {
                        return cb(null, path.join(distPath, 'source', name), srcPkgs[0]);
                    }
                }
            }
        },
        function (lpath, pkg, cb) {
            if (snapshot.remote) {
                return 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 {
                return callback(null, resultPkgs);
            }
        } else {
            return 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 j = 0; j < srcPkgs.length; j++) {
        var pkg = srcPkgs[j];

        var depList = pkg.installDepList;
        for (var k = 0; k < depList.length; k++) {
            var dep = depList[k];
            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 {
                return callback(null, resultPkgs);
            }
        } else {
            return 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 j = 0; j < srcPkgs.length; j++) {
        var pkg = srcPkgs[j];

        var depList = pkg.installDepList;
        for (var k = 0; k < depList.length; k++) {
            var dep = depList[k];
            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) {
                    return cb(err, null);
                } else {
                    return 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);
                        return cb2(null);
                    } else {
                        // NOTE. MUST controll whether ignore invalid snapshots or not
                        return cb2(null);
                    }
                });
            }, function (err) {
                cb(err, result);
            });
        }], function (err, snapshots) {
        if (tempPath !== null) {
            extfs.remove(tempPath, function () {
                callback(err, snapshots);
            });
        } else {
            return 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) {
                    return cb(null, null);
                } else {
                    return 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 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 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;
}


function convertRawToTizenSnapshot(rawSnapshot) {
    // convert queried snapshot object to TizenSnapshot
    var tizenSnapshot = new TizenSnapshot(rawSnapshot.name);

    tizenSnapshot.type = rawSnapshot.attribute;
    tizenSnapshot.time = rawSnapshot.time;
    tizenSnapshot.path = rawSnapshot.path;

    tizenSnapshot.newPkgs = rawSnapshot.newPkgs || [];
    tizenSnapshot.updatedPkgs = rawSnapshot.updatedPkgs || [];
    tizenSnapshot.removedPkgs = rawSnapshot.removedPkgs || [];

    tizenSnapshot.options = {};

    // changelog, origin, uploader, remote, __ENVIRONMENTS
    var regex = /__[A-Z]/;
    _.each(rawSnapshot.options, function (value, key) {
        var match = regex.exec(key);

        if (match) {
            tizenSnapshot.options[match.input] = value;
        } else {
            tizenSnapshot[key] = value;
        }
    });

    var osPackages = {};
    var envs = rawSnapshot.options['__ENVIRONMENTS'];
    _.each(envs, function (os) {
        osPackages[os] = {};
    });

    if (!_.isEmpty(rawSnapshot.artifacts)) {
        var archives = _.filter(rawSnapshot.artifacts, function (artifact) {
            return (artifact.type === 'archive');
        });

        var archivePackages = [];
        _.each(archives, function (archive) {
            archivePackages.push(archive.name);
        });
        // assign archive packages into TizenSnapshot
        tizenSnapshot.archivePackages = archivePackages;

        var binaryArtifacts = _.filter(rawSnapshot.artifacts, function (artifact) {
            return (artifact.type === 'binary');
        });

        var binaryPackages = _.map(binaryArtifacts, function (artifact) {
            return convertRawToTizenPackage(artifact);
        });

        _.each(envs, function (os) {
            _.each(binaryPackages, function (pkg) {
                if (pkg.os === os) {
                    osPackages[os][pkg.name] = pkg;
                }
            });
        });
    }

    // assign binary packages into TizenSnapshot
    tizenSnapshot.osPackages = osPackages;

    return tizenSnapshot;
}


function convertRawToTizenPackage(rawPackage) {
    // convert queried artifacts to Package
    var tizenPkg = {
        name: rawPackage.name,
        version: rawPackage.version,
        os: rawPackage.environment,
        type: rawPackage.type,
        attr: rawPackage.attribute,
        path: rawPackage.path,
        size: rawPackage.size,
        checksum: rawPackage.checksum,
        desc: rawPackage.description,
        options: {}
    };

    var regex = /__[A-Z]/;
    _.each(rawPackage.options, function (value, key) {
        if (key === 'changelog') {
            return;
        }

        var match = regex.exec(key);
        if (match) {
            tizenPkg.options[match.input] = value;
        } else {
            tizenPkg[key] = value;
        }
    });

    return tizenPkg;
}

function convertTizenPackageToRaw(pkg, distName) {
    // convert queried artifacts to Package
    var opts = _.clone(pkg);
    var filename = TizenUtils.getPackageFileNameFromInfo(pkg.name, pkg.version, pkg.os);

    delete opts.name;
    delete opts.distName;
    delete opts.version;
    delete opts.os;
    delete opts.attr;
    delete opts.filename;
    delete opts.size;
    delete opts.checksum;
    delete opts.desc;

    _.each(opts.options, function (value, key) {
        opts[key] = value;
    });
    delete opts.options;

    return {
        name: pkg.name,
        distName: distName,
        type: 'binary',
        version: pkg.version,
        environment: pkg.os,
        attribute: pkg.attr || null,
        status: pkg.status || 'OPEN',
        file: filename,
        path: pkg.path,
        size: pkg.size,
        checksum: pkg.checksum,
        description: pkg.desc,
        options: opts
    };
}

function searchSnapshotList(conditions, callback) {
    if (!conditions.distName) {
        callback(new DError('TREPO003'), []);
        return;
    }

    conditions.noArtifacts = true;

    if (conditions.type) {
        conditions.attribute = conditions.type;
        delete conditions.type;
    }

    dibs.rpc.datamgr.searchSnapshots(conditions, function (err1, results) {
        if (err1) {
            return callback(err1, []);
        } else {
            // convert snapshot objects into TizenSnapshot objects
            var snapshots = _.map(results, function (rawSnapshot) {
                return convertRawToTizenSnapshot(rawSnapshot);
            });
            return callback(null, snapshots);
        }
    });
}


function searchSnapshots(conditions, callback) {
    if (!conditions.distName) {
        callback(new DError('TREPO003'), []);
        return;
    }

    if (conditions.type) {
        conditions.attribute = conditions.type;
        delete conditions.type;
    }

    dibs.rpc.datamgr.searchSnapshots(conditions, function (err1, results) {
        if (err1) {
            return callback(err1, []);
        } else {
            // convert snapshot objects into TizenSnapshot objects
            var snapshots = _.map(results, function (rawSnapshot) {
                return convertRawToTizenSnapshot(rawSnapshot);
            });

            return callback(null, snapshots);
        }
    });
}


function searchBinaryArtifacts(conditions, callback) {
    if (!conditions.distName) {
        callback(new DError('TREPO003'), []);
        return;
    }

    if (conditions.type !== 'binary') {
        callback(new DError('TREPO032', { type: conditions.type }), []);
        return;
    }

    dibs.rpc.datamgr.searchArtifacts(conditions, function (err, results) {
        if (err) {
            return callback(err, []);
        } else {
            var pkgs = _.map(results, function (rawPackage) {
                return convertRawToTizenPackage(rawPackage);
            });

            return callback(err, pkgs);
        }
    });

}


function searchArchiveArtifacts(conditions, callback) {
    if (!conditions.distName) {
        callback(new DError('TREPO003'), []);
        return;
    }

    if (conditions.type !== 'archive') {
        callback(new DError('TREPO032', { type: conditions.type }), []);
        return;
    }

    var archivePackages = [];

    dibs.rpc.datamgr.searchArtifacts(conditions, function (err, results) {
        if (err) {
            return callback(err);
        } else {
            _.each(results, function (artifact) {
                archivePackages.push(artifact.name);
            });
            return callback(null, archivePackages);
        }
    });
}


function removeArtifactWithDB(conditions, callback) {
    if (!conditions.distName) {
        callback(new DError('TREPO003'), []);
        return;
    }

    async.waterfall([
        function (cb) {
            dibs.rpc.datamgr.searchArtifacts(conditions, function (err, results) {
                cb(err, results);
            });
        },
        function (artifacts, cb) {
            async.each(artifacts, function (artifact, cb1) {
                dibs.rpc.datamgr.deleteArtifact(artifact.id, function (err1) {
                    cb1(err1);
                });
            },
            function (err) {
                cb(err);
            });
        }
    ],
    function (err) {
        callback(err);
    });
}
