/**
 * repo-cli.js
 * Copyright (c) 2017 Samsung Electronics Co., Ltd. All rights reserved.
 *
 * Contact:
 * Sungmin Kim <sm.art.kim@samsung.com>
 * Jonghwan Park <iwin100.park@samsung.com>
 * Kitae Kim <kt920.kim@samsung.com>
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 * Contributors:
 * - S-Core Co., Ltd
**/

'use strict';

var async = require('async');
var fs = require('fs');
var extfs = require('fs-extra');
var optimist = require('optimist');
var path = require('path');
var _ = require('underscore');
var util = require('util');
var winston = require('winston');
require('date-format-lite');

var Monitor = require('../../lib/monitor.js');
var utils = require('../../lib/utils.js');

var Zip = require('../dibs.core/zip.js');
var TsRepo = require('../org.tizen.ts.cli.repository/ts-repo.js');
var tsUtil = require('../org.tizen.ts.base.common/util.js');


var log = new (winston.Logger)({
    transports: [
        new (winston.transports.File)({
            filename: '.ts-cli.log',
            timestamp: function () {
                var now = new Date();

                return now.format('YYYY-MM-DD hh:mm:ss.SS');
            },
            handleExceptions: true,
            json: false,
            level: 'verbose'
        }),
        new (winston.transports.Console)({
            handleExceptions: true,
            colorize: true,
            formatter: function (opts) {
                return winston.config.colorize(opts.level, opts.level.toUpperCase() + ' ' + (opts.message ? opts.message : '') +
                    (opts.meta && Object.keys(opts.meta).length ? '\n\t' + JSON.stringify(opts.meta) : ''));
            },
            level: 'info'
        })
    ],
    exceptionHandlers: [new winston.transports.File({
        filename: 'logException.log'
    })],
    exitOnError: false
});


// get host information
var usageMessage =
    'This tool is command line interface for control repository\n\n' +
    'Usage: $0 <SUBCOMMAND> [OPTS]\n\n' +
    'Subcommands:\n' +
    '  diff-snapshots       Diff packages between two snapshots\n' +
    '  create-patchset      Generate patchset image between two snapshots\n' +
    '  validate-snapshots   Validate snapshots after distribution synchronization\n' +
    '  apply-patchset       Apply patchset image into target repository\n\n' +
    'Subcommand usage:\n' +
    '  diff-snapshots       repo-cli diff-snapshots -s <latest repository server url> -t <oldest repository server url> [-S <source snapshot name>] [-T <target snapshot name>]\n' +
    '  create-patchset      repo-cli create-patchset -s <latest repository path> -t <oldest repository path> [-S <source snapshot name>] [-T <target snapshot name>]\n' +
    '                                                [-m <changelog message>][-o <patchset snapshot name>] [--exclude <package type>]\n';

var argv = optimist.usage(usageMessage)
    .describe('s', 'Source repository that have newest snapshots. { http://127.0.0.1/repo/develop or local filesystem }')
    .describe('S', 'Source snapshot name. { Tizen_Studio_1.1 }')
    .describe('t', 'Target repository that have oldest snapshots. { http://download.tizen.org/sdk/tizenstudio/official/ or local filesystem}')
    .describe('T', 'Target snapshot name. { Tizen_Studio_1.0 }')
    .describe('o', 'Specify snapshot name to replace source snapshot name')
    .describe('m', 'Specify changelog messages. Use \'_\' as a separator in case of using whitespace. { Tizen Studio 1.0 }')
    .describe('e', 'exclude package type. { archive, none }')
    .describe('E', 'Specify exclude-meta package list')
    .describe('h', 'show help')
    .string('S')
    .string('T')
    .string('m')
    .string('c')
    .string('E')
    .default('exclude', 'archive')
    .alias({ h: 'help', s: 'source', t: 'target', S: 'source-snapshot', T: 'target-snapshot', e: 'exclude', m: 'msg', o: 'output-snapshot', E: 'exclude-meta' })
    .argv;

var subCmds = argv._;
if (argv.h) {
    optimist.showHelp();
    process.exit(0);
}

// validate sub command
if (!subCmds || subCmds.length === 0) {
    log.error('Sub-Command must be specified!');
    process.exit(-1);
}

// init variables
var sourceServerURL = argv.source;
var sourceSnapshot = argv.S;
var targetServerURL = argv.target;
var targetSnapshot = argv.T;
var patchsetSnapshot = argv.o;

var excludeOpt = argv.exclude;
var changelogMsg = argv.m;
var excludeMeta = argv.E;

var monitor = new Monitor({
    onProgress: function (info, cb) {
        if (info.logType) {
            log[info.logType](info.log);
        }
        cb(null);
    }
});

var SNAPSHOT_INFO_FILE = 'snapshot.info';

// entry
switch (subCmds[0]) {
case 'diff-snapshots':
    handleDiffSnapshot(sourceServerURL, targetServerURL, sourceSnapshot, targetSnapshot, excludeMeta);
    break;
case 'create-patchset':
    handleCreatePatchset(sourceServerURL, targetServerURL, sourceSnapshot, targetSnapshot, changelogMsg, excludeMeta);
    break;
default:
    log.error("Invalid sub-command!: '" + subCmds + "'");
    process.exit(-1);
}

/*
 *  source repo: a repository path that have latest snapshots. ex) http://127.0.0.1/repo/tizen_studio_1.3
 *  target repo: a repository path that have previous snapshots. ex) http://127.0.0.1/repo/tizen_studio_1.2
 */
function handleDiffSnapshot(sourceRepo, targetRepo, sourceSnapshotName, targetSnapshotName, excludeMetaList) {
    var usageMsg = 'repo-cli diff-snapshots -s <latest repository server url> -t <oldest repository server url> ' +
        '[-S <source snapshot name>] [-T <target snapshot name>]';

    // check arguments
    if (!sourceRepo || !sourceRepo.length || sourceRepo.length === 0) {
        monitor.updateProgress({ log: 'Source repository URL must be specified!', logType: 'error' });
        monitor.updateProgress({ log: usageMsg, logType: 'error' });
        process.exit(-1);
    }

    if (!targetRepo || !targetRepo.length || targetRepo.length === 0) {
        monitor.updateProgress({ log: 'Target repository URL must be specified!', logType: 'error' });
        monitor.updateProgress({ log: usageMsg, logType: 'error' });
        process.exit(-1);
    }

    if (!sourceSnapshotName) {
        sourceSnapshotName = null;
    }

    if (!targetSnapshotName) {
        targetSnapshotName = null;
    }

    log.info('# - Compare snapshots and packages between two repository');

    getUpdatedPackages(sourceRepo, targetRepo, sourceSnapshotName, targetSnapshotName, { excludeMetaList: excludeMetaList }, function (err, results) {
        if (err) {
            monitor.updateProgress({ log: '# - Package comparison between two snapshots failure!', logType: 'error' });
            monitor.updateProgress({ log: err, logType: 'error' });
            process.exit(-1);
        } else {
            var comparedPkgs = results.comparedPkgs;
            var diffPkgCount = 0;
            var newPkgCount = 0;

            _.each(comparedPkgs.diff, function (pkg) {
                if (pkg.isNewPackage) {
                    newPkgCount++;
                } else {
                    diffPkgCount++;
                }
            });

            var msg = ' # - New: ' + newPkgCount + ' Updated: ' + diffPkgCount + ' Removed: ' + comparedPkgs.removed.length +
                    ' Rest: ' + comparedPkgs.rest.length + ' Error: ' + comparedPkgs.trouble.length;
            monitor.updateProgress({ log: msg, logType: 'info' });
            monitor.updateProgress({ log: '# - Package comparison between two snapshots success!', logType: 'info' });
            process.exit(0);
        }
    });
}


function handleCreatePatchset(sourceRepo, targetRepo, sourceSnapshotName, targetSnapshotName, changelogMsg, excludeMetaList) {
    if (!sourceRepo || !sourceRepo.length || sourceRepo.length === 0) {
        monitor.updateProgress({ log: 'Source repository must be specified!', logType: 'error' });
        process.exit(-1);
    }

    if (!targetRepo || !targetRepo.length || targetRepo.length === 0) {
        monitor.updateProgress({ log: 'Target repository must be specified!', logType: 'error' });
        process.exit(-1);
    }

    if (!sourceSnapshotName) {
        sourceSnapshotName = null;
    }

    if (!targetSnapshotName) {
        targetSnapshotName = null;
    }

    var osList = [];

    var oldDist = null;
    var oldSnapshot = null;

    var newDist = null;
    var newSnapshot = null;
    // imageSnapshot does not have development packages
    var imageSnapshot = null;

    var comparedPkgs = [];

    var tempRepoPath = path.join(process.cwd(), 'tmp', 'repo');
    var tempDistPath = null;
    // var downloadPath = path.join(tempPath, 'download');

    async.waterfall([
        function (cb) {
            getUpdatedPackages(sourceRepo, targetRepo, sourceSnapshotName, targetSnapshotName, { excludeMetaList: excludeMetaList }, function (err, results) {
                if (!err) {
                    oldDist = results.oldDist;
                    oldSnapshot = results.oldSnapshot;
                    newDist = results.newDist;
                    newSnapshot = results.newSnapshot;
                    imageSnapshot = results.imageSnapshot;
                    comparedPkgs = results.comparedPkgs;

                    if (patchsetSnapshot) {
                        imageSnapshot.name = patchsetSnapshot;
                        imageSnapshot.path = '/snapshots/' + patchsetSnapshot;
                        imageSnapshot.type = 'manual';
                    }

                    osList = _.allKeys(oldSnapshot.osPackages);
                }
                cb(err);
            });
        },
        function (cb) {
            tempDistPath = path.join(tempRepoPath, oldDist.name);
            var tempRepo = new TsRepo({
                location: tempDistPath
            });

            tempRepo.load(function (err) {
                if (err) {
                    monitor.updateProgress({ log: err, logType: 'error' });
                }
                cb(null);
            });
        },
        function (cb) {
            var osInfoPath = path.join(tempDistPath, 'os_info');
            fs.writeFile(osInfoPath, osList.join('\n'), cb);
        },
        function (cb) {
            oldDist.snapshots.push(imageSnapshot);
            saveSnapshotInfo(tempDistPath, oldDist.snapshots, cb);
        },
        function (cb) {
            var snapshotPath = path.join(tempDistPath, imageSnapshot.path);
            monitor.updateProgress(' # - Create ' + snapshotPath);
            extfs.ensureDir(snapshotPath, function (err) {
                cb(err, snapshotPath);
            });
        },
        function (snapshotPath, cb) {
            var osPackages = _.clone(oldSnapshot.osPackages);
            _.each(osList, function (os) {
                _.each(imageSnapshot.osPackages[os], function (pkgInfo) {
                    if (os === pkgInfo.os) {
                        if (!osPackages[os][pkgInfo.name]) {
                            monitor.updateProgress(' # - New Package ' + pkgInfo.name + ' (' + os + ')');
                        }

                        osPackages[os][pkgInfo.name] = pkgInfo;
                    // } else {
                    //     console.log(pkgInfo.name + ' ' + pkgInfo.os + ' : ' + os);
                    }
                });
            });

            saveOsPackages(snapshotPath, osPackages, function (err, results) {
                cb(err, results);
            });
        },
        function (pkgListFiles, cb) {
            // Copy pkg_list_xxx files into distribution.
            async.each(pkgListFiles, function (pkgListFile, cb1) {
                extfs.copy(pkgListFile, path.join(tempDistPath, path.basename(pkgListFile)), cb1);
            }, cb);
        },
        function (cb) {
            if (changelogMsg) {
                // write changelog message.
                changelogMsg = changelogMsg.split('_').join(' ');

                monitor.updateProgress(' # - Write changelog ' + changelogMsg + ' into file');
                var changlogFile = path.join(tempDistPath, 'changes', imageSnapshot.name + '.log');
                var message = 'Changes:\n - ' + changelogMsg;
                fs.writeFile(changlogFile, message, cb);
            } else {
                cb(null);
            }
        },
        function (cb) {
            var diffPackages = comparedPkgs.diff;
            newSnapshot.downloadPackages(newDist.path, diffPackages, path.join(tempDistPath, 'binary'), monitor, function (err, results) {
                cb(err, results);
            });
            // extfs.readdir(path.join(tempDistPath, 'binary'), function (err, results) {
            //     var downloadPkgs = _.compact(_.map(results, function (file) {
            //         if (path.extname(file) === '.zip') {
            //             return path.join(tempDistPath, 'binary', file);
            //         } else {
            //             return null;
            //         }
            //     }));

            //     cb(null, downloadPkgs);
            // });
        },
        function (downloadPkgs, cb) {
            async.eachLimit(downloadPkgs, 2, function (pkgPath, cb1) {
                var pkgInfo = tsUtil.package.getInfoFromPackageFileName(pkgPath);

                tsUtil.package.getPkgListFromFile(path.join(tempDistPath, 'pkg_list_' + pkgInfo.os), function (err1, pkgList) {
                    if (!err1) {
                        var pkg = _.findWhere(pkgList, { name: pkgInfo.name });
                        validatePackage(pkgPath, pkg, cb1);
                    } else {
                        cb1(err1);
                    }
                });
            },
            function (err) {
                cb(err);
            });
        },
        function (cb) {
            var imageFile = path.join(process.cwd(), imageSnapshot.name + '.zip');

            async.series([
                function (cb1) {
                    utils.removePathIfExist(imageFile, cb1);
                },
                function (cb1) {
                    monitor.updateProgress({ log: '# Compress patchset image file: ' + imageFile, logType: 'info' });
                    Zip.compress(imageFile, tempRepoPath, function (err) {
                        if (err) {
                            log.error(err);
                        } else {
                            // TODO: need to create zip info about size, checksum and etc.
                        }
                        cb1(err);
                    });
                }
            ], cb);
        }
    ],
    function (err) {
        monitor.updateProgress({ log: '# Remove temporary repository directory', logType: 'info' });
        utils.removePathIfExist(tempRepoPath, function (err1) {
            if (err1) {
                monitor.updateProgress({ log: err1, logType: 'error' });
            }

            if (err) {
                monitor.updateProgress({ log: '# - Patchset image generation failure!', logType: 'error' });
                process.exit(-1);
            } else {
                monitor.updateProgress({ log: '# - Patchset image generation success!', logType: 'info' });
                process.exit(0);
            }
        });
    });
}


// private
function loadRepo(repoLocation, baseSnapshotName, callback) {
    var distribution = null;
    var distName = path.basename(repoLocation);

    var snapshot = null;
    var repo = new TsRepo({
        location: repoLocation
    });

    async.series([
        function (cb) {
            log.info(' # - Open repostiory for ' + repoLocation);
            repo.open(function (err) {
                if (!err) {
                    distribution = repo.distributions[distName];
                    if (!distribution) {
                        err = new Error('# - Failed to get \'' + distName + '\'');
                    }
                } else {
                    log.error(err);
                }
                cb(err);
            });
        },
        function (cb) {
            log.info(' # - Get a snapshot from ' + repoLocation);
            distribution.searchSnapshots({ distPath: distribution.path, name: baseSnapshotName }, function (err, snapshots) {
                if (!err) {
                    if (snapshots.length === 0) {
                        err = new Error('# - Any snapshot does not exist in \'' + distName + '\'');
                    } else {
                        snapshot = snapshots[0];
                        log.info(' # - Getting a snapshot successfully \'' + snapshot.name + '\'');
                    }
                } else {
                    log.error(err);
                }
                cb(err);
            });
        }
    ],
    function (err) {
        callback(err, { distribution: distribution, snapshot: snapshot });
    });
}


function getUpdatedPackages(sourceRepo, targetRepo, sourceSnapshotName, targetSnapshotName, options, callback) {
    var comparedPkgs = null;

    var oldDist = null;
    var newDist = null;

    var oldSnapshot = null;
    var newSnapshot = null;
    var imageSnapshot = null;

    var osList = [];

    var excludePkgList = null;
    if (options && options.excludeMetaList) {
        excludePkgList = options.excludeMetaList.split(',');
    }

    async.waterfall([
        function (cb) {
            loadRepo(targetRepo, targetSnapshotName, function (err, results) {
                if (!err) {
                    oldDist = results.distribution;
                    oldSnapshot = results.snapshot;
                }
                cb(err);
            });
        },
        function (cb) {
            loadRepo(sourceRepo, sourceSnapshotName, function (err, results) {
                if (!err) {
                    newDist = results.distribution;
                    newSnapshot = results.snapshot;
                    osList = _.allKeys(newSnapshot.osPackages);
                }
                cb(err);
            });
        },
        function (cb) {
            createImageSnapshot(newDist, newSnapshot, osList, { excludePkgList: excludePkgList }, function (err, results) {
                imageSnapshot = _.clone(newSnapshot);
                imageSnapshot.osPackages = results;
                cb(err, imageSnapshot);
            });
        },
        function (imageSnapshot, cb) {
            monitor.updateProgress({ log: '# Compare packages between two snapshots', logType: 'info' });

            comparedPkgs = compareSnapshots(oldSnapshot, imageSnapshot, osList);
            if (comparedPkgs.diff.length === 0) {
                cb(new Error('There is no difference between two snapshots'));
            } else {
                cb(null);
            }
        },
        function (cb) {
            monitor.updateProgress({ log: '# Changed Packages: ' + comparedPkgs.diff.length, logType: 'info' });
            createPackageChangeLogFile(comparedPkgs, function (err) {
                cb(err);
            });
        }
    ],
    function (err) {
        if (err) {
            monitor.updateProgress({ log: err, logType: 'error' });
        }
        callback(err, {
            oldDist: oldDist,
            newDist: newDist,
            oldSnapshot: oldSnapshot,
            newSnapshot: newSnapshot,
            imageSnapshot: imageSnapshot,
            comparedPkgs: comparedPkgs
        });
    });
}


function createImageSnapshot(newDist, newSnapshot, osList, options, callback) {
    var metaPkgList = [];
    var installTypePkgNameList = [];
    var imgPkgList = [];

    var excludePkgList = null;
    if (options && options.excludePkgList) {
        excludePkgList = options.excludePkgList;
    }

    var skipIntegrityCheck = false;
    if (options && options.skipIntegrityCheck) {
        skipIntegrityCheck = true;
    }

    var osPackages = {};

    async.eachLimit(osList, 1, function (targetOS, cb) {
        osPackages[targetOS] = {};

        async.waterfall([
            function (cb1) {
                _.each(newSnapshot.osPackages[targetOS], function (pkg) {
                    if (pkg.attr === 'root' || pkg.attr === 'mandatory') {
                        metaPkgList.push(pkg);
                    } else if (pkg.attr === 'install') {
                        installTypePkgNameList.push(pkg);
                    }
                });

                if (excludePkgList && !_.isEmpty(excludePkgList)) {
                    var excludeMetapackageList = [];
                    monitor.updateProgress(' # - Excluding those meta packages');
                    monitor.updateProgress(' # - ' + _.values(excludePkgList));

                    var tempList = _.filter(metaPkgList, function (metaPkg) {
                        return (_.indexOf(excludePkgList, metaPkg.name) !== -1);
                    });

                    newSnapshot.getAllInstallDependentPackages(tempList, {}, monitor, function (err, results) {
                        if (err) {
                            monitor.updateProgress({ log: err, logType: 'error' });
                            cb1(err);
                        } else {
                            _.each(results, function (pkg) {
                                if (pkg.attr === 'root') {
                                    excludeMetapackageList.push(pkg);
                                }
                            });
                            cb1(null, _.difference(metaPkgList, _.union(tempList, excludeMetapackageList)));
                        }
                    });
                } else {
                    imgPkgList = _.union(metaPkgList, installTypePkgNameList);
                    cb1(null, metaPkgList);
                }
            },
            function (results, cb1) {
                // get install dependency pkg list
                if (excludePkgList && !_.isEmpty(excludePkgList)) {
                    var tempList = _.filter(metaPkgList, function (metaPkg) {
                        return (_.indexOf(excludePkgList, metaPkg.name) !== -1);
                    });
                    newSnapshot.getAllInstallDependentPackagesWithExcludeList(results, tempList, monitor, cb1);
                } else {
                    var opts = {};
                    if (skipIntegrityCheck) {
                        opts.skipIntegrityCheck = skipIntegrityCheck;
                    }
                    newSnapshot.getAllInstallDependentPackages(results, opts, monitor, cb1);
                }
            },
            function (downloadPkgs, cb1) {
                imgPkgList = _.union(imgPkgList, downloadPkgs, installTypePkgNameList);
                if (excludePkgList && !_.isEmpty(excludePkgList)) {
                    _.each(imgPkgList, function (pkg) {
                        pkg.installDepList = _.reject(pkg.installDepList, function (depPkg) {
                            return _.contains(excludePkgList, depPkg.packageName);
                        });
                    });
                    cb1(null, imgPkgList);
                } else {
                    cb1(null, imgPkgList);
                }
            }
        ],
        function (err1, imgPkgList) {
            _.each(imgPkgList, function (pkg) {
                osPackages[targetOS][pkg.name] = pkg;
            });
            cb(err1);
        });
    },
    function (err) {
        callback(err, osPackages);
    });
}


function createPackageChangeLogFile(packages, callback) {
    var diffPkgFile = path.join(process.cwd(), 'diff-package.list');

    async.waterfall([
        function (cb) {
            fs.exists(diffPkgFile, function (exist) {
                cb(null, exist);
            });
        },
        function (exist, cb) {
            if (exist) {
                monitor.updateProgress({ log: ' # - Remove ' + path.basename(diffPkgFile), logType: 'debug' });
                fs.unlink(diffPkgFile, function (err) {
                    if (err) {
                        log.error(err);
                    }
                    cb(err);
                });
            } else {
                cb(null);
            }
        },
        function (cb) {
            monitor.updateProgress({ log: ' # - Create diff-package.list file', logType: 'debug' });
            var sortedDiffPackages = _.sortBy(packages.diff, 'path');
            var newPackages = _.filter(sortedDiffPackages, function (pkg) {
                return pkg.isNewPackage;
            });
            var updatePackages = _.filter(sortedDiffPackages, function (pkg) {
                return !pkg.isNewPackage;
            });
            var removedPackages = _.sortBy(packages.removed, 'path');
            var unchangedPackages = _.sortBy(packages.rest, 'path');
            var conflictPackages = _.sortBy(packages.trouble, 'path');

            async.series([
                function (cb1) {
                    fs.writeFile(diffPkgFile, '<New Packages>\n', { encoding: 'utf-8', flag: 'a' }, cb1);
                },
                function (cb1) {
                    async.each(newPackages,
                        function (data, cb2) {
                            fs.writeFile(diffPkgFile, (util.inspect(data.path) + '\n'), { encoding: 'utf-8', flag: 'a' }, cb2);
                        },
                        function (err) {
                            cb1(err);
                        });
                },
                function (cb1) {
                    fs.writeFile(diffPkgFile, '<Version Update Packages>\n', { encoding: 'utf-8', flag: 'a' }, cb1);
                },
                function (cb1) {
                    async.each(updatePackages,
                        function (data, cb2) {
                            fs.writeFile(diffPkgFile, (util.inspect(data.path) + '\n'), { encoding: 'utf-8', flag: 'a' }, cb2);
                        },
                        function (err) {
                            cb1(err);
                        });
                },
                function (cb1) {
                    fs.writeFile(diffPkgFile, '<Removed Packages>\n', { encoding: 'utf-8', flag: 'a' }, cb1);
                },
                function (cb1) {
                    async.each(removedPackages,
                        function (data, cb2) {
                            fs.writeFile(diffPkgFile, (util.inspect(data.path) + '\n'), { encoding: 'utf-8', flag: 'a' }, cb2);
                        },
                        function (err) {
                            cb1(err);
                        });
                },
                function (cb1) {
                    fs.writeFile(diffPkgFile, '<Version Conflict Packages>\n', { encoding: 'utf-8', flag: 'a' }, cb1);
                },
                function (cb1) {
                    async.each(conflictPackages,
                        function (data, cb2) {
                            fs.writeFile(diffPkgFile, (util.inspect(data.path) + '\n'), { encoding: 'utf-8', flag: 'a' }, cb2);
                        },
                        function (err) {
                            cb1(err);
                        });
                },
                function (cb1) {
                    fs.writeFile(diffPkgFile, '<Version No Update Packages>\n', { encoding: 'utf-8', flag: 'a' }, cb1);
                },
                function (cb1) {
                    async.each(unchangedPackages,
                        function (data, cb2) {
                            fs.writeFile(diffPkgFile, (util.inspect(data.path) + '\n'), { encoding: 'utf-8', flag: 'a' }, cb2);
                        },
                        function (err) {
                            cb1(err);
                        });
                }
            ],
            function (err) {
                cb(err);
            });

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


function compareSnapshots(srcSnapshot, destSnapshot, osList) {
    var diffPackages = [];
    var restPackages = [];
    var removedPackages = [];
    var troublePackages = [];

    if (!srcSnapshot.osPackages || !destSnapshot.osPackages) {
        log.error('Cannot get osPackages object from srcSnapshot or destSnapshot');
        return diffPackages;
    }

    _.each(osList, function (os) {
        _.each(destSnapshot.osPackages[os], function (pkg) {
            if (srcSnapshot.osPackages[os][pkg.name]) {
                var oldPkg = srcSnapshot.osPackages[os][pkg.name];

                if (tsUtil.package.compareVersion(pkg.version, oldPkg.version) > 0) {
                    pkg.isNewPackage = false;
                    diffPackages.push(pkg);
                } else if (tsUtil.package.compareVersion(pkg.version, oldPkg.version) === 0) {
                    restPackages.push(pkg);
                } else {
                    troublePackages.push(pkg);
                }
            } else {
                if (pkg.os === os) {
                    pkg.isNewPackage = true;
                    diffPackages.push(pkg);
                } else {
                    log.warn(' - # \'' + pkg.name + '\' is not supported in \'' + os + '\'.');
                }
            }
        });

        _.each(srcSnapshot.osPackages[os], function (pkg) {
            if (!destSnapshot.osPackages[os][pkg.name]) {
                removedPackages.push(pkg);
            }
        });
    });

    if (troublePackages.length !== 0) {
        monitor.updateProgress({ log: ' # - Packages in new snapshot are lower than old snapshot!', logType: 'error' });
        _.each(troublePackages, function (pkg) {
            monitor.updateProgress({ log: pkg.name + ' : ' + pkg.version + ' : ' + pkg.os, logType: 'error' });
        });
    }

    return { diff: diffPackages, rest: restPackages, trouble: troublePackages, removed: removedPackages };
}


function saveSnapshotInfo(distPath, snapshots, callback) {
    var snapshotInfoPath = path.join(distPath, SNAPSHOT_INFO_FILE);

    var snapshotInfos = [];

    _.each(snapshots, function (snapshot) {
        snapshotInfos.push('name : ' + snapshot.name);
        snapshotInfos.push('time : ' + snapshot.time);
        snapshotInfos.push('type : ' + snapshot.type);
        if (snapshot.origin) {
            snapshotInfos.push('origin: ' + snapshot.origin);
        }
        snapshotInfos.push('path : ' + snapshot.path);
        snapshotInfos.push('');
    });

    fs.appendFile(snapshotInfoPath, snapshotInfos.join('\n'), callback);
}


function saveOsPackages(snapshotPath, osPackages, callback) {
    var osPkgList = _.map(osPackages, function (packages, os) {
        return [os, util.package.pkgListString(packages)];
    });

    var pkgListFiles = [];

    async.each(osPkgList, function (osPkg, cb) {
        var os = osPkg[0];
        var contents = osPkg[1];
        var pkgListFile = path.join(snapshotPath, 'pkg_list_' + os);
        fs.writeFile(pkgListFile, contents, function (err1) {
            if (!err1) {
                pkgListFiles.push(pkgListFile);
            }
            cb(err1);
        });
    },
    function (err) {
        callback(err, pkgListFiles);
    });
}


function getRepositoryInfo(serverURL) {
    var result = {};

    var sIndex = serverURL.indexOf('/snapshots/');

    if (sIndex !== -1) {
        result.baseURL = path.dirname(serverURL.substring(0, sIndex));
        result.distName = path.basename(serverURL.substring(0, sIndex));
        result.snapshotName = path.basename(serverURL);
    } else {
        result.baseURL = path.dirname(serverURL);
        result.distName = path.basename(serverURL);
        result.snapshotName = null;
    }

    return result;
}


function validatePackage(pkgPath, pkgInfo, callback) {
    fs.exists(pkgPath, function (exists) {
        if (!exists) {
            callback(new Error(path.basename(pkgPath) + ' does not exist'));
        } else {
            monitor.updateProgress({ log: ' # - Verify checksum & size: ' + path.basename(pkgPath), logType: 'info' });
            compareChecksumAndSize(pkgPath, pkgInfo, function (err1) {
                callback(err1);
            });
        }
    });
}


function compareChecksumAndSize(filePath, pkgInfo, callback) {
    async.series([
        function (cb) {
            // check checksum
            utils.getCheckSum(filePath, function (err, checksum) {
                if (err) {
                    cb(err);
                } else {
                    if (checksum === pkgInfo.checksum) {
                        cb(null);
                    } else {
                        cb(new Error(path.basename(filePath) + ' checksum is different from ' + pkgInfo.checksum));
                    }
                }
            });
        },
        function (cb) {
            fs.stat(filePath, function (err, stats) {
                if (err) {
                    cb(err);
                } else {
                    if (stats.size === pkgInfo.size) {
                        cb(null);
                    } else {
                        cb(new Error(path.basename(filePath) + ' size is different from ' + pkgInfo.size));
                    }
                }
            });
        }
    ],
    function (err) {
        callback(err);
    });
}
