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 === 'symlink';
}

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;
