/**
 * distribution.js
 * Copyright (c) 2000 - 2015 Samsung Electronics Co., Ltd. All rights reserved.
 *
 * Contact:
 * DongHee Yang <donghee.yang@samsung.com>
 * Sungmin Kim <sm.art.kim@samsung.com>
 * Jiil Hyoun <jiil.hyoun@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 async = require('async');
var path = require('path');
var synchronized = require('synchronized');
var _ = require('underscore');

var dibs = require('../../core/dibs');
var dfs = require('../../plugins/dibs.dist-fs/dist-fs.js');
var Snapshot = require('./stand-alone-snapshot.js');
var Utils = require('../../lib/utils.js');
var Package = require('../org.tizen.common/package.js');
var TizenUtils = require('../org.tizen.common/tizen_utils.js');
var DError = require('../../core/exception.js');
var Release = require('./release.js');


module.exports.getAllPkgFileRefCountHash = getAllPkgFileRefCountHash;
module.exports.makeDistributionInfo = makeDistributionInfo;
module.exports.load = loadDistributions;
module.exports.create = createDistribution;
module.exports.remove = removeDistribution;
module.exports.registerPackages = registerPackages;
module.exports.registerPackagesFromLocal = registerPackagesFromLocal;
module.exports.removePackages = removePackages;
module.exports.registerTempFiles = registerTempFiles;
module.exports.generateSnapshot = generateSnapshot;
module.exports.loadSnapshot = loadSnapshot;
module.exports.loadRemoteSnapshot = loadRemoteSnapshot;
module.exports.removeSnapshot = removeSnapshots;
module.exports.loadChangelog = loadChangelog;
module.exports.loadRemoteChangelog = loadRemoteChangelog;

module.exports.prepareToDownloadPackage = prepareToDownloadPackage;
module.exports.loadRemote = loadRemoteDistributions;
module.exports.downloadPackageFromRemote = downloadPackageFromRemote;
module.exports.downloadPackagesFromRemote = downloadPackagesFromRemote;
module.exports.preparePackagesFromRemote = preparePackagesFromRemote;

module.exports.createRelease = createRelease;
module.exports.removeRelease = removeRelease;
module.exports.updateRelease = updateRelease;
module.exports.searchReleases = searchReleases;
module.exports.getRelease = getRelease;


function TizenDistribution(name) {
    var self = this;

    this.name = name;
    this.type = 'tizen';
    this.time = null;
    this.snapshots = [];
    this.releases = [];
    this.pkgFileRefCountHash = {};
    this.latestSnapshot = null;
    this.path = null;
    this.syncMethod = null;
    this.syncURL = null;
    this.syncDistName = null;
    this.syncPeriod = -1;
    this.syncPrefix = null;
    this.options = {};
}

/*
 * clean up distribution.info file
 */
function makeDistributionInfo(repoPath, callback) {
    fs.writeFile(path.join(repoPath, 'distribution.info'), '', callback);
}

/*
 * load all distributions snapshots and latestSnapshot
 */
function loadDistributions(repoPath, callback) {
    async.waterfall([
        function (wcb) {
            loadDistributionInfo(repoPath, wcb);
        },
        function (dists, wcb) {
            async.eachSeries(dists, function (dist, cb) {
                loadDistribution(dist.name, repoPath, function (err, dist2) {
                    if (!err) {
                        dist.snapshots = dist2.snapshots;
                        // choose the latest 'auto' snapshot
                        // TODO reverse find for performance
                        for (var i = 0; i < dist.snapshots.length; i++) {
                            if (dist.snapshots[i].type === 'auto') {
                                dist.latestSnapshot = dist.snapshots[i];
                            }
                        }
                        // if not, choose latest snapshot
                        // TODO merge upper logic
                        if (dist.latestSnapshot === null && dist.snapshots.length > 0) {
                            dist.latestSnapshot = dist.snapshots[dist.snapshots.length - 1];
                        }
                        dist.releases = dist2.releases;
                    }
                    if (dist.latestSnapshot) {
                        loadSnapshot(dist.latestSnapshot.name, dist, function (err, nSnapshot) {
                            dist.latestSnapshot.archivePackages = nSnapshot.archivePackages;
                            dist.latestSnapshot.osPackages = nSnapshot.osPackages;
                            Snapshot.loadChangelog(path.join(dist.path, 'changes', dist.latestSnapshot.name + '.log'), function (changelog) {
                                dist.latestSnapshot.changeLog = changelog;
                                cb(err);
                            });
                        });
                    } else {
                        cb(err);
                    }
                });
            }, function (err) {
                wcb(err, dists);
            });

        }], callback);

}

/*
 * parse distributionInfo file
 */
function loadDistributionInfo(repoPath, callback) {
    loadDistributionInfoFile(path.join(repoPath, 'distribution.info'), repoPath, callback);
}


function loadDistributionInfoFile(infoFilePath, repoPath, callback) {
    fs.readFile(infoFilePath, {
        encoding: 'utf8'
    },
        function (err, contents) {
            if (err) {
                callback(err, null);
            } else {
                callback(err, loadDistributionInfoString(contents, repoPath));
            }
        });
}

function loadDistributionInfoString(contents, repoPath) {
    var lines = contents.split('\n');

    var dists = [];
    var newDist = null;
    for (var i = 0; i < lines.length; i++) {
        var line = lines[i];
        var toks = line.split(/[: ]+/);
        if (toks[0] === 'name') {
            if (newDist !== null) {
                dists.push(newDist);
            }
            newDist = new TizenDistribution(toks[1]);
            if (Utils.isURL(repoPath)) {
                newDist.path = repoPath + '/' + newDist.name;
            } else {
                newDist.path = path.join(repoPath, newDist.name);
            }
        } else if (toks[0] === 'time') {
            newDist.time = toks[1];
        } else if (toks[0] === 'id') {
            newDist.uid = toks[1];
        } else {
            // ignore
        }
    }
    if (newDist !== null) {
        dists.push(newDist);
    }

    return dists;
}


/*
 * return distribution object fill snapshots
 */
function loadDistribution(distName, repoPath, callback) {
    var dist = null;
    var distPath = path.join(repoPath, distName);

    async.series([
        // check directory
        function (cb) {
            fs.exists(distPath, function (exist) {
                if (exist) {
                    dist = new TizenDistribution(distName);
                    cb(null);
                } else {
                    cb(new DError('TREPO021', {
                        distPath: distPath
                    }));
                }
            });
        },
        // load snapshots
        function (cb) {
            Snapshot.load(distPath, function (err, snapshots) {
                if (!err) {
                    dist.snapshots = snapshots;
                }
                cb(err);
            });
        },
        // load release
        function (cb) {
            Release.load(distPath, function (err, releases) {
                if (!err) {
                    dist.releases = releases;
                }
                cb(err);
            });
        }], function (err) {
        callback(err, dist);
    });
}


function loadChangelog(dist, snapshotName, callback) {
    Snapshot.loadChangelog(path.join(dist.path, 'changes', snapshotName + '.log'), callback);
}

function loadRemoteChangelog(dist, snapshotName, callback) {
    callback(null);
}

/*
 * make repository struct
 * repoPath/
 *      dist_name/
 *          snapshots/
 *          binary/
 *          changes/
 *          source/
 *          snapshot.info
 *
 * and add info to distribution.info file
 */
function createDistribution(dname, opts, repoPath, callback) {
    var newObj = new TizenDistribution(dname);
    newObj.time = Utils.generateTimeStamp();
    newObj.path = path.join(repoPath, newObj.name);
    if (opts.SYNC_URL) {
        newObj.syncURL = opts.SYNC_URL;
    }
    if (opts.SYNC_DIST_NAME) {
        newObj.syncDistName = opts.SYNC_DIST_NAME;
    }
    if (opts.SYNC_PERIOD) {
        newObj.syncPeriod = opts.SYNC_PERIOD;
    }

    if (opts.SYNC_PREFIX) {
        newObj.syncPrefix = opts.SYNC_PREFIX;
    }

    async.series([
        // create distribution directories
        function (scb) {
            fs.mkdir(path.join(repoPath, dname), scb);
        },
        function (scb) {
            fs.mkdir(path.join(repoPath, dname, 'snapshots'), scb);
        },
        function (scb) {
            fs.mkdir(path.join(repoPath, dname, 'binary'), scb);
        },
        function (scb) {
            fs.mkdir(path.join(repoPath, dname, 'changes'), scb);
        },
        function (scb) {
            fs.mkdir(path.join(repoPath, dname, 'source'), scb);
        },
        // create snapshot.info file
        function (scb) {
            fs.writeFile(path.join(repoPath, dname, 'snapshot.info'), '', scb);
        },
        // modify distribution.info
        function (scb) {
            appendDistributionInfo(newObj, repoPath, scb);
        }
    ],
    function (err) {
        callback(err, newObj);
    });
}

function appendDistributionInfo(newDist, repoPath, callback) {
    async.waterfall([
        function (wcb) {
            loadDistributionInfo(repoPath, wcb);
        },
        function (dists, wcb) {
            var lines = [];
            for (var i = 0; i < dists.length; i++) {
                var dist = dists[i];
                lines.push('name : ' + dist.name);
                lines.push('time : ' + dist.time);
                if (dist.uid) {
                    lines.push('id : ' + dist.uid);
                }
                lines.push('');
            }

            lines.push('name : ' + newDist.name);
            lines.push('time : ' + newDist.time);

            fs.writeFile(path.join(repoPath, 'distribution.info'), lines.join('\n'),
                function (err) {
                    wcb(err);
                });
        }
    ],
    function (err) {
        callback(err);
    });

}


function removeDistribution(dname, opts, repoPath, callback) {
    async.waterfall([
        async.apply(removeDistributionInfo, dname, repoPath),
        async.apply(extfs.remove, path.join(repoPath, dname))
    ], callback);
}


function removeDistributionInfo(dname, repoPath, callback) {
    async.waterfall([
        function (wcb) {
            loadDistributionInfo(repoPath, wcb);
        },
        function (dists, wcb) {
            var filtered = dists.filter(function (e) {
                return e.name === dname;
            });
            if (filtered.length > 0) {
                dists.splice(dists.indexOf(filtered[0]), 1);
            }

            var lines = [];
            for (var i = 0; i < dists.length; i++) {
                var dist = dists[i];
                lines.push('name : ' + dist.name);
                lines.push('time : ' + dist.time);
                if (dist.uid) {
                    lines.push('id : ' + dist.uid);
                }
                lines.push('');
            }

            fs.writeFile(path.join(repoPath, 'distribution.info'), lines.join('\n'), wcb);
        }
    ],
    function (err) {
        callback(err);
    });


}


function registerTempFiles(rpaths, dist, opts, progress, callback) {
    async.waterfall([
        function (cb) {
            if (progress) {
                progress('## Get temporary file information...');
            }
            preparePackageFiles(rpaths, progress, cb);
        },
        function (fileInfos, cb) {
            registerTempFilesWithSync(fileInfos, dist, opts, progress, cb);
        }
    ], function (err, sname) {
        callback(err, sname);
    });
}


function registerTempFilesWithSync(fileInfos, dist, opts, progress, callback) {
    var copiedFiles = [];
    var newSnapshot = null;
    var tempPathName = '/temp/jobs/' + opts.tempJobId + '/';
    synchronized(dist.name,
        function (cb) {
            async.waterfall([
                function (cb2) {
                    if (progress) {
                        progress('## Appending package(s) to new snapshot...');
                    }
                    opts.PATH_PREFIX = tempPathName;
                    opts.TEMP_NAME = 'TEMP_JOB' + opts.tempJobId;
                    // set 'forceUpload' option to ignore version-check
                    opts.forceUpload = true;
                    opts.skipIntegrityCheck = dist.options.SKIP_INTEGRITY_CHECK;
                    Snapshot.appendPackages(fileInfos, dist.latestSnapshot, opts,
                        function (err, s) {
                            if (!err) {
                                newSnapshot = s;
                            }
                            cb2(err);
                        });
                },
                function (cb2) {
                    if (progress) {
                        progress('## Copying temporary files to repository...');
                    }
                    copyFilesToRepository(fileInfos, opts, dist.path,
                        function (err, copied) {
                            copiedFiles = copied;
                            cb2(err);
                        });
                },
                function (cb2) {
                    if (progress) {
                        progress('## Updating snapshot...');
                    }
                    var relateTempPath = path.join('temp', 'jobs', opts.tempJobId.toString());
                    Snapshot.saveTempSnapshot(newSnapshot, dist.path, relateTempPath,
                        function (err) {
                            if (!err) {
                                addSnapshotToDist(newSnapshot, dist);
                                dist.pkgFileRefCountHash = Snapshot.updateRefCountHash(dist.pkgFileRefCountHash, newSnapshot);
                            }
                            cb2(err);
                        });
                }
            ], function (err) {
                if (err) {
                    rollbackPackageRegisteration(copiedFiles, function () {
                        cb(err);
                    });
                } else {
                    cb(err);
                }
            });
        },
        function (err) {
            callback(err, newSnapshot);
        });
}


function addSnapshotToDist(newSnapshot, dist) {
    var updatedPackages = Snapshot.getUpdatedPackages(newSnapshot, dist.latestSnapshot);

    dibs.thisServer.emitEvent({
        event: 'TIZEN_REPO_SNAPSHOT_GENERATED',
        distributionName: dist.name,
        updatedPackages: updatedPackages
    });

    var snapshotInfo = _.clone(newSnapshot);

    // reset unnecessary info
    snapshotInfo.archivePackages = null;
    snapshotInfo.osPackages = null;
    snapshotInfo.removedPkgs = null;
    snapshotInfo.newPkgs = null;
    snapshotInfo.updatedPkgs = null;

    dist.snapshots.push(snapshotInfo);
    // only "auto" snapshot can be the latest
    if (newSnapshot.type === 'auto') {
        dist.latestSnapshot = newSnapshot;
    }
}


function registerPackages(rpaths, dist, opts, progress, callback) {
    async.waterfall([
        function (cb) {
            if (progress) {
                progress('## Preparing package files...');
            }
            preparePackageFiles(rpaths, progress, cb);
        },
        function (fileInfos, cb) {
            if (progress) {
                progress('## Registering package(s)...');
            }
            registerPackagesWithSync(fileInfos, dist, opts, progress, cb);
        }
    ], function (err, snapshot) {
        callback(err, snapshot);
    });
}

function registerPackagesFromLocal(paths, dist, opts, progress, callback) {
    async.waterfall([
        function (cb) {
            if (progress) {
                progress('## Preparing package files...');
            }
            preparePackageFiles2(paths, progress, cb);
        },
        function (fileInfos, cb) {
            if (progress) {
                progress('## Registering package(s)...');
            }
            dist.options.SKIP_INTEGRITY_CHECK = true;
            registerPackagesWithSync(fileInfos, dist, opts, progress, cb);
        }
    ], function (err, snapshot) {
        callback(err, snapshot);
    });
}


function registerPackagesWithSync(fileInfos, dist, opts, progress, callback) {
    var copiedFiles = [];
    var newSnapshot = null;

    synchronized(dist.name,
        function (cb) {
            async.waterfall([
                function (cb2) {
                    if (progress) {
                        progress('## Appending package(s) to new snapshot...');
                    }
                    opts.skipIntegrityCheck = dist.options.SKIP_INTEGRITY_CHECK;
                    Snapshot.appendPackages(fileInfos, dist.latestSnapshot, opts,
                        function (err, s) {
                            if (!err) {
                                newSnapshot = s;
                            }
                            cb2(err, fileInfos);
                        });
                },
                function (fileInfos, cb2) {
                    if (progress) {
                        progress('## Copying package files to repository...');
                    }
                    copyFilesToRepository(fileInfos, opts, dist.path,
                        function (err, copied) {
                            copiedFiles = copied;
                            cb2(err);
                        });
                },
                function (cb2) {
                    if (progress) {
                        progress('## Updating snapshot...');
                    }
                    saveSnapshot(newSnapshot, dist, cb2);
                }
            ], function (err, snapshot) {
                if (err) {
                    if (progress) {
                        progress('## Rolling back by error...');
                    }
                    rollbackPackageRegisteration(copiedFiles, function () {
                        cb(err, null);
                    });
                } else {
                    cb(err, snapshot);
                }
            });
        },
        function (err, snapshot) {
            callback(err, snapshot);
        });
}


// callback(err, copied)
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) {
            callback(err, 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);
    });
}

/*
 * get packages info
 * callback(err, fileInfos)
 * fileInfos :
 * {
 *      dfsPathA : pkgA,
 *      dfsPathB : pkgB,
 *      ...
 * }
 */
function preparePackageFiles(rpaths, progress, callback) {
    var fileInfos = {};
    async.eachLimit(rpaths, 3,
        function (dfsPath, cb) {
            async.waterfall([
                function (cb1) {
                    downloadSingleFileIfNotExist(dfsPath, progress, cb1);
                },
                function (cb1) {
                    extractPackageManifestsFromSingleFile(dfsPath, progress, cb1);
                }], function (err, info) {
                if (!err) {
                    fileInfos[dfsPath] = info;
                }
                cb(err);
            });
        }, function (err) {
            callback(err, fileInfos);
        });

}

function preparePackageFiles2(paths, progress, callback) {
    var fileInfos = {};
    async.eachLimit(paths, 8,
        function (path, cb) {
            async.waterfall([
                function (cb1) {
                    extractPackageManifestsFromSingleFileReal(path, progress, cb1);
                }], function (err, info) {
                if (!err) {
                    fileInfos[path] = info;
                }
                cb(err);
            });
        }, function (err) {
            callback(err, fileInfos);
        });

}


function downloadSingleFileIfNotExist(dfsPath, progress, callback) {
    var realPath = dfs.getRealFilePath(dfsPath);
    fs.exists(realPath, function (exist) {
        if (exist) {
            if (progress) {
                progress(' * exists in local...' + dfsPath);
            }
            callback(null);
        } else {
            if (progress) {
                progress(' * transfering...' + dfsPath);
            }
            dfs.getFile(null, dfsPath, callback);
        }
    });
}

//callback(err, pkg)
function extractPackageManifestsFromSingleFileReal(realPath, progress, callback) {
    if (path.extname(realPath) === '.zip') {
        if (progress) {
            progress(' * extracting pkginfo.manifest...' + realPath);
        }
        Package.getPkgInfoFromPkgFile(realPath, function (err, pkg) {
            if (!err) {
                callback(err, pkg);
            } else {
                callback(err, null);
            }
        });
    } else {
        callback(null, null);
    }
}

//callback(err, pkg)
function extractPackageManifestsFromSingleFile(dfsPath, progress, callback) {
    var realPath = dfs.getRealFilePath(dfsPath);
    if (path.extname(realPath) === '.zip') {
        if (progress) {
            progress(' * [START] extracting pkginfo.manifest...' + dfsPath);
        }
        Package.getPkgInfoFromPkgFile(realPath, function (err, pkg) {
            if (err && err.errno !== 'TIZENCOMMON003') {
                if (progress) {
                    progress(' ! [FAIL] extracting pkginfo.manifest...' + dfsPath);
                    progress(' ! Error: ' + err);
                }
                return callback(err, null);
            } else {
                var fileInfo = dfs.getFileInfo(dfsPath);
                if (fileInfo && fileInfo.checksum && fileInfo.size) {
                    pkg.checksum = fileInfo.checksum;
                    pkg.size = fileInfo.size;
                    pkg.origin = 'local';

                    if (progress) {
                        progress(' * [SUCCESS] extracting pkginfo.manifest...' + dfsPath);
                    }
                    return callback(null, pkg);
                } else {
                    if (progress) {
                        progress(' ! [FAIL] extracting pkginfo.manifest...' + dfsPath);
                    }
                    return callback(new Error('fileInfo is undefined (' + dfsPath + ')'), null);
                }
            }
        });
    } else {
        callback(null, null);
    }
}

/*
 * remove same name of old snapshot
 * and add new snapshot to dist
 */
function saveSnapshot(newSnapshot, dist, callback) {
    Snapshot.save(newSnapshot, dist.path,
        function (err) {
            if (!err) {
                addSnapshotToDist(newSnapshot, dist);
                dist.pkgFileRefCountHash = Snapshot.updateRefCountHash(dist.pkgFileRefCountHash, newSnapshot);
                callback(err, newSnapshot);
            } else {
                callback(err, null);
            }
        });
}


function rollbackPackageRegisteration(copiedFiles, callback) {
    async.each(copiedFiles, function (cfile, ecb) {
        fs.unlink(cfile, ecb);
    }, callback);
}


function removePackages(names, dist, opts, callback) {
    async.waterfall([
        // remove from pkg_lists
        function (cb) {
            opts.skipIntegrityCheck = dist.options.SKIP_INTEGRITY_CHECK;
            Snapshot.removePackages(names, dist.latestSnapshot, opts, cb);
        },
        // save snapshot
        function (newSnapshot, cb) {
            saveSnapshot(newSnapshot, dist, cb);
        }
    ], function (err, sname) {
        if (err) {
            callback(err, null);
        } else {
            callback(err, sname);
        }
    });

}


function generateSnapshot(name, dist, opts, callback) {

    async.waterfall([
        function (cb) { //-> callback(err)
            if (name && _.find(dist.snapshots, function (e) {
                return (e.name === name);
            })) { // name exist
                if (opts.force === undefined || opts.force === false) {
                    cb(new DError('TREPO022', {
                        snapshot: name
                    }));
                } else {
                    removeSnapshots([name], dist, {}, cb);
                }
            } else {
                cb(null);
            }
        },
        function (cb) { //-> callback(err,snapshot);
            if (opts.refSnapshot) {
                var filtered = dist.snapshots.filter(function (e) {
                    return (e.name === opts.refSnapshot);
                });
                if (filtered.length === 0) {
                    cb(new DError('TREPO023', {
                        ref: opts.refSnapshot
                    }), null);
                } else {
                    loadSnapshot(opts.refSnapshot, dist, function (err, loaded) {
                        loaded.name = _.clone(filtered[0].name);
                        loaded.time = _.clone(filtered[0].time);
                        cb(err, loaded);
                    });
                }
            } else {
                cb(null, dist.latestSnapshot);
            }
        },
        function (sourceSnapshot, wcb) {
            // create & save
            var newSnapshot = Snapshot.create(name, sourceSnapshot, opts);
            saveSnapshot(newSnapshot, dist, wcb);
        }],
        function (err, newSnapshot) {
            callback(err, newSnapshot);
        });
}

function loadSnapshot(name, dist, callback) {
    var snapshots = dist.snapshots.filter(function (e) {
        return (e.name === name);
    });

    if (snapshots.length === 0) {
        callback(new DError('TREPO023', {
            ref: name
        }), null);
        return;
    }
    Snapshot.loadSnapshot(path.join(dist.path, snapshots[0].path), callback);
}

function notIncludeSnapshotNameList(nameList, snapshotInfoList) {
    var snapshotInfoHash = _.indexBy(snapshotInfoList, 'name');
    return _.reject(nameList, function (name) {
        return snapshotInfoHash[name];
    });
}

function removePkgFileRefCountHash(removeRefCountHash, RefCountHash) {
    var removeFilePath = [];
    _.each(removeRefCountHash, function (count, path) {
        RefCountHash[path] -= count;
        if (RefCountHash[path] < 1) {
            removeFilePath.push(path);
        }
    });
    return removeFilePath;
}

function removeSnapshots(nameList, dist, opts, callback) { //-> callback(err)
    // check index is ready
    if (dist.indexIsReady === false) {
        callback(new DError('TREPO030', {
            dist: dist.name
        }));
        return;
    }
    var orphanNames = notIncludeSnapshotNameList(nameList, dist.snapshots);

    if (orphanNames.length !== 0) {
        callback(new DError('TREPO023', {
            ref: orphanNames.join(', ')
        }));
        return;
    }

    var removeSnapshot = function (cb) {
        Snapshot.remove(nameList, dist.path, cb);
    };
    var removeOrphanPackages = function (removeRefCountHash, cb) {
        var removePackageList = removePkgFileRefCountHash(removeRefCountHash, dist.pkgFileRefCountHash);
        _.each(removePackageList, function (removePkg) {
            dist.pkgFileRefCountHash = _.omit(dist.pkgFileRefCountHash, removePkg);
        });
        var removePackagePathList = _.map(removePackageList,
            function (pkgPath) {
                return path.join(dist.path, pkgPath);
            });
        async.eachLimit(removePackagePathList, 10, extfs.remove, cb);
    };

    // remove from snapshot
    dist.snapshots = Snapshot.removeSnapshotInfo(nameList, dist.snapshots);
    async.waterfall([
        removeSnapshot,
        removeOrphanPackages
    ], function (err) {
        callback(err);
    });

}


function prepareToDownloadPackage(name, dist, opts, callback) {
    var filtered = dist.snapshots.filter(function (e) {
        return e.name === opts.snapshotName;
    });
    if (filtered.length === 0) {
        callback(new DError('TREPO023', {
            ref: opts.snapshotName
        }), null);
        return;
    }
    var snapshot = filtered[0];

    async.waterfall([
        function (wcb) {
            if (!snapshot.archivePackages || snapshot.archivePackages.length === 0 || !snapshot.osPackages || _.isEmpty(snapshot.osPackages)) {
                if (snapshot.remote) { // snapshot.remote means already loaded
                    wcb(null, snapshot);
                } else {
                    loadSnapshot(snapshot.name, dist, wcb);
                }
            } else {
                wcb(null, snapshot);
            }
        },
        function (loadedSnapshot, wcb) {
            Snapshot.prepareToDownload(name, loadedSnapshot, dist.path, opts.os, wcb);
        }],
        function (err, dfsPath) {
            callback(err, dfsPath);
        });
}


function loadRemoteDistributions(repoURL, repoOptions, callback) {
    async.waterfall([
        function (cb) {
            loadRemoteDistributionInfo(repoURL, function (err, dists) {
                if (!err) {
                    if (repoOptions.distName !== undefined) {
                        cb(null, dists.filter(function (dist) {
                            return dist.name === repoOptions.distName;
                        }));
                    } else {
                        cb(null, dists);
                    }
                } else {
                    cb(err, null);
                }
            });
        },
        function (dists, cb) {
            if (dists.length === 0) {
                cb(null, dists);
            } else {
                async.eachSeries(dists, function (dist, cb) {
                    loadRemoteDistribution(dist.name, repoURL, repoOptions, function (err, dist2) {
                        if (!err) {
                            dist.snapshots = dist2.snapshots;
                            for (var i = 0; i < dist.snapshots.length; i++) {
                                // choose the latest 'auto' snapshot
                                for (var i = 0; i < dist.snapshots.length; i++) {
                                    if (dist.snapshots[i].type === 'auto') {
                                        dist.latestSnapshot = dist.snapshots[i];
                                    }
                                }
                                // if not, choose latest snapshot
                                if (dist.latestSnapshot === null && dist.snapshots.length > 0) {
                                    dist.latestSnapshot = dist.snapshots[dist.snapshots.length - 1];
                                }
                            }
                        }
                        cb(err);
                    });
                }, function (err) {
                    cb(err, dists);
                });
            }
        }], function (err, dists) {
        callback(err, dists);
    });
}


function loadRemoteDistributionInfo(repoURL, callback) {
    Utils.getTextFromUrl(repoURL + '/distribution.info', function (err, distString) {
        if (err) {
            callback(new DError('TREPO031', {
                url: repoURL
            }), null);
        } else {
            callback(err, loadDistributionInfoString(distString, repoURL));
        }
    });
}


function loadRemoteDistribution(distName, repoURL, repoOptions, callback) {
    // create distribution
    var dist = new TizenDistribution(distName);

    // load snapshots
    var distURL = repoURL + '/' + distName;
    Snapshot.loadRemote(distURL, repoOptions, function (err, snapshots) {
        if (!err) {
            dist.snapshots = snapshots;
        }
        callback(err, dist);
    });
}


function loadRemoteSnapshot(snapshotName, dist, repoURL, callback) {
    var snapshots = dist.snapshots.filter(function (e) {
        return (e.name === snapshotName);
    });

    if (snapshots.length === 0) {
        callback(new DError('TREPO023', {
            ref: snapshotName
        }), null);
        return;
    }

    // load snapshots
    var distURL = repoURL + '/' + dist.name;
    var snapshotDirURL = distURL + '/' + snapshots[0].path;

    Snapshot.loadRemoteSnapshot(snapshotDirURL, callback);
}


function downloadPackagesFromRemote(pkgs, snapshot, targetDir, distPath, progress, callback) {

    async.mapLimit(pkgs, 8,
        function (pkg, cb) {
            if (pkg.name === undefined) {
                downloadPackageFromRemote2(pkg, snapshot, null, targetDir, distPath, progress, cb);
            } else {
                downloadPackageFromRemote2(pkg.name, snapshot, pkg.os, targetDir, distPath, progress, cb);
            }
        },
        function (err, result) {
            callback(err, result);
        });
}


function downloadPackageFromRemote2(name, snapshot, os, targetDir, distPath, progress, callback) {
    if (progress) {
        progress(' - ' + name + (os ? ('(' + os + ')') : ''));
    }

    async.waterfall([
        function (cb) {
            if (Utils.isURL(distPath)) {
                Snapshot.prepareToDownload(name, snapshot, distPath, os, cb);
            } else {
                dibs.rpc.repo.downloadRemotePackage(name, {
                    repoType: 'tizen',
                    distName: path.basename(distPath),
                    snapshotName: snapshot.name,
                    os: os
                }, cb);
            }
        },
        // get files from remote repository
        function (dfsPath, cb) {
            if (Utils.isURL(dfsPath)) {
                Utils.download(dfsPath, targetDir, cb);
            } else {
                var tPath = path.join(targetDir, path.basename(dfsPath));
                dfs.getFile(tPath, dfsPath, function (err) {
                    cb(err, tPath);
                });
            }
        }
    ], function (err, downloadedPath) {
        callback(err, downloadedPath);
    });
}


function downloadPackageFromRemote(name, dist, opts, callback) {
    var targetDir = opts.targetDir === undefined ? '.' : opts.targetDir;

    async.waterfall([
        function (cb) {
            prepareToDownloadPackage(name, dist, opts, cb);
        },
        // get files from remote repository
        function (dfsPath, cb) {
            if (Utils.isURL(dfsPath)) {
                Utils.download(dfsPath, targetDir, cb);
            } else {
                var tPath = path.join(targetDir, path.basename(dfsPath));
                dfs.getFile(tPath, dfsPath, function (err) {
                    cb(err, tPath);
                });
            }
        }
    ], function (err, downloadedPath) {
        callback(err, downloadedPath);
    });
}


function preparePackagesFromRemote(pkgs, snapshot, distPath, progress, callback) {

    async.mapLimit(pkgs, 8,
        function (pkg, cb) {
            if (pkg.name === undefined) {
                preparePackageFromRemote(pkg, snapshot, null, distPath, progress, cb);
            } else {
                preparePackageFromRemote(pkg.name, snapshot, pkg.os, distPath, progress, cb);
            }
        },
        function (err, result) {
            callback(err, result);
        });
}


function preparePackageFromRemote(name, snapshot, os, distPath, progress, callback) {
    if (progress) {
        progress(' - ' + name + (os ? ('(' + os + ')') : ''));
    }

    async.waterfall([
        function (cb) {
            Snapshot.prepareToDownload(name, snapshot, distPath, os, cb);
        }
    ], function (err, dfsPath) {
        callback(err, dfsPath);
    });
}


function getAllPkgFileRefCountHash(distName, repoPath, callback) {
    var distPath = path.join(repoPath, distName);
    var loadDistSnapshot = function (cb) { // -> callback(err, snapshotList)
        Snapshot.load(distPath, cb);
    };
    var getDistPkgFileRefCountHash = function (snapshotList, cb) { // -> callback(err, RefCountHash)
        Snapshot.getPkgFileRefCountHash(distPath, snapshotList, cb);
    };

    async.waterfall([
        loadDistSnapshot,
        getDistPkgFileRefCountHash
    ], callback);
}


function createRelease(releaseName, dist, opts, callback) {
    // check if the name already exists
    var result = dist.releases.filter(function (rel) {
        return rel.name === releaseName;
    });

    if (result.length > 0) {
        return callback(new DError('TREPO028', {
            name: releaseName
        }), null);
    }

    // get target snapshot
    var snapshotName = opts.SNAPSHOT_NAME ? opts.snapshotName : null;
    if (opts.SNAPSHOT_NAME) {
        snapshotName = opts.SNAPSHOT_NAME;
        var matched = dist.snapshots.filter(function (s) {
            return s.name === snapshotName;
        });
        if (matched.length === 0) {
            return callback(new Error('Snapshot not exist'), null);
        }
    } else if (dist.latestSnapshot) {
        snapshotName = dist.latestSnapshot.name;
    } else {
        return callback(new Error('No snapshots'), null);
    }

    Release.create(releaseName, snapshotName, dist.path, opts, function (err, release) {
        if (!err) {
            dist.releases.push(release);
        }
        callback(err, release);
    });
}


function removeRelease(releaseName, dist, opts, callback) {
    // check if the name does not exist
    var matched = dist.releases.filter(function (rel) {
        return rel.name === releaseName;
    });

    if (matched.length === 0) {
        return callback(new DError('TREPO027', {
            name: releaseName
        }), null);
    }

    // cannot rollback
    dist.releases.splice(dist.releases.indexOf(matched[0]), 1);
    Release.remove(matched[0], dist.path, opts, function (err, release) {
        callback(err, release);
    });
}


function updateRelease(release, dist, opts, callback) {
    // check if the name does not exist
    var oldReleases = dist.releases.filter(function (rel) {
        return rel.name === release.name;
    });

    if (oldReleases.length === 0) {
        return callback(new DError('TREPO027', {
            name: release.name
        }), null);
    }

    Release.update(release, dist.path, opts, function (err) {
        if (!err) {
            dist.releases.splice(
                dist.releases.indexOf(oldReleases[0]), 1,
                release);
        }
        callback(err, release);
    });
}


function searchReleases(dist, opts, callback) {
    var result = dist.releases.filter(function (rel) {
        if (opts.name && opts.name !== rel.name) {
            return false;
        }
        if (opts.type && opts.type !== rel.type) {
            return false;
        }
        return true;
    });

    callback(null, result);
}


function getRelease(releaseName, dist, callback) {
    var result = dist.releases.filter(function (rel) {
        return rel.name === releaseName;
    });

    if (result.length > 0) {
        callback(null, result[0]);
    } else {
        callback(new DError('TREPO027', {
            name: releaseName
        }), null);
    }
}
