var path = require('path');
var fs = require('./fs');
var zlib = require('zlib');
var DecompressZip = require('decompress-zip');
var tar = require('tar-fs');
var Q = require('q');
var mout = require('mout');
var junk = require('junk');
var createError = require('./createError');
var createWriteStream = require('fs-write-stream-atomic');
var destroy = require('destroy');
var tmp = require('tmp');

// This forces the default chunk size to something small in an attempt
// to avoid issue #314
zlib.Z_DEFAULT_CHUNK = 1024 * 8;

var extractors;
var extractorTypes;

extractors = {
    '.zip': extractZip,
    '.tar': extractTar,
    '.tar.gz': extractTarGz,
    '.tgz': extractTarGz,
    '.gz': extractGz,
    'application/zip': extractZip,
    'application/x-zip': extractZip,
    'application/x-zip-compressed': extractZip,
    'application/x-tar': extractTar,
    'application/x-tgz': extractTarGz,
    'application/x-gzip': extractGz
};

extractorTypes = Object.keys(extractors);

function extractZip(archive, dst) {
    var deferred = Q.defer();

    new DecompressZip(archive)
        .on('error', deferred.reject)
        .on('extract', deferred.resolve.bind(deferred, dst))
        .extract({
            path: dst,
            follow: false,           // Do not follow symlinks (#699)
            filter: filterSymlinks   // Filter symlink files
        });

    return deferred.promise;
}

function extractTar(archive, dst) {
    var deferred = Q.defer();

    var stream = fs.createReadStream(archive);

    var reject = function (error) {
        destroy(stream);
        deferred.reject(error);
    };

    stream.on('error', reject)
        .pipe(tar.extract(dst, {
            ignore: isSymlink, // Filter symlink files
            dmode: 0555,       // Ensure dirs are readable
            fmode: 0444        // Ensure files are readable
        }))
        .on('error', reject)
        .on('finish', function (result) {
            destroy(stream);
            deferred.resolve(dst);
        });

    return deferred.promise;
}

function extractTarGz(archive, dst) {
    var deferred = Q.defer();

    var stream = fs.createReadStream(archive);

    var reject = function (error) {
        destroy(stream);
        deferred.reject(error);
    };

    stream.on('error', reject)
        .pipe(zlib.createGunzip())
        .on('error', reject)
        .pipe(tar.extract(dst, {
            ignore: isSymlink, // Filter symlink files
            dmode: 0555,       // Ensure dirs are readable
            fmode: 0444        // Ensure files are readable
        }))
        .on('error', reject)
        .on('finish', function (result) {
            destroy(stream);
            deferred.resolve(dst);
        });

    return deferred.promise;
}

function extractGz(archive, dst) {
    var deferred = Q.defer();

    var stream = fs.createReadStream(archive);

    var reject = function (error) {
        destroy(stream);
        deferred.reject(error);
    };
    stream.on('error', reject)
        .pipe(zlib.createGunzip())
        .on('error', reject)
        .pipe(createWriteStream(dst))
        .on('error', reject)
        .on('finish', function (result) {
            destroy(stream);
            deferred.resolve(dst);
        });

    return deferred.promise;
}

function isSymlink(entry) {
    return entry.type === 'SymbolicLink';
}

function filterSymlinks(entry) {
    return entry.type !== 'SymbolicLink';
}

function getExtractor(archive) {
    // Make the archive lower case to match against the types
    // This ensures that upper-cased extensions work
    archive = archive.toLowerCase();

    var type = mout.array.find(extractorTypes, function (type) {
        return mout.string.endsWith(archive, type);
    });

    return type ? extractors[type] : null;
}

function isSingleDir(dir) {
    return Q.nfcall(fs.readdir, dir)
        .then(function (files) {
            var singleDir;

            // Remove any OS specific files from the files array
            // before checking its length
            files = files.filter(junk.isnt);

            if (files.length !== 1) {
                return false;
            }

            singleDir = path.join(dir, files[0]);

            return Q.nfcall(fs.stat, singleDir)
                .then(function (stat) {
                    return stat.isDirectory() ? singleDir : false;
                });
        });
}

function moveDirectory(srcDir, destDir) {
    return Q.nfcall(fs.readdir, srcDir)
        .then(function (files) {
            var promises = files.map(function (file) {
                var src = path.join(srcDir, file);
                var dst = path.join(destDir, file);

                return Q.nfcall(fs.rename, src, dst);
            });

            return Q.all(promises);
        })
        .then(function () {
            return Q.nfcall(fs.rmdir, srcDir);
        });
}

// -----------------------------

function canExtract(src, mimeType) {
    if (mimeType && mimeType !== 'application/octet-stream') {
        return !!getExtractor(mimeType);
    }

    return !!getExtractor(src);
}

// Available options:
// - keepArchive: true to keep the archive afterwards (defaults to false)
// - keepStructure: true to keep the extracted structure unchanged (defaults to false)
function extract(src, dst, opts) {
    var extractor;
    var promise;

    opts = opts || {};
    extractor = getExtractor(src);

    // Try to get extractor from mime type
    if (!extractor && opts.mimeType) {
        extractor = getExtractor(opts.mimeType);
    }

    // If extractor is null, then the archive type is unknown
    if (!extractor) {
        return Q.reject(createError('File ' + src + ' is not a known archive', 'ENOTARCHIVE'));
    }

    // Extract to a temporary directory in case of file name clashes
    return Q.nfcall(tmp.dir, {
        template: dst + '-XXXXXX',
        mode: 0777 & ~process.umask()
    }).then(function (tempDir) {
        // nfcall may return multiple callback arguments as an array
        return Array.isArray(tempDir) ? tempDir[0] : tempDir;
    }).then(function (tempDir) {

        // Check archive file size
        promise = Q.nfcall(fs.stat, src)
            .then(function (stat) {
                if (stat.size <= 8) {
                    throw createError('File ' + src + ' is an invalid archive', 'ENOTARCHIVE');
                }

                // Extract archive
                return extractor(src, tempDir);
            });

        // Remove archive
        if (!opts.keepArchive) {
            promise = promise
                .then(function () {
                    return Q.nfcall(fs.unlink, src);
                });
        }

        // Move contents from the temporary directory
        // If the contents are a single directory (and we're not preserving structure),
        // move its contents directly instead.
        promise = promise
            .then(function () {
                return isSingleDir(tempDir);
            })
            .then(function (singleDir) {
                if (singleDir && !opts.keepStructure) {
                    return moveDirectory(singleDir, dst);
                } else {
                    return moveDirectory(tempDir, dst);
                }
            });

        // Resolve promise to the dst dir
        return promise.then(function () {
            return dst;
        });
    });
}

module.exports = extract;
module.exports.canExtract = canExtract;
