var path = require('path');
var fs = require('graceful-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');

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

    fs.createReadStream(archive)
    .on('error', deferred.reject)
    .pipe(tar.extract(dst, {
        ignore: isSymlink  // Filter symlink files
    }))
    .on('error', deferred.reject)
    .on('finish', deferred.resolve.bind(deferred, dst));

    return deferred.promise;
}

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

    fs.createReadStream(archive)
    .on('error', deferred.reject)
    .pipe(zlib.createGunzip())
    .on('error', deferred.reject)
    .pipe(tar.extract(dst, {
        ignore: isSymlink  // Filter symlink files
    }))
    .on('error', deferred.reject)
    .on('finish', deferred.resolve.bind(deferred, dst));

    return deferred.promise;
}

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

    fs.createReadStream(archive)
    .on('error', deferred.reject)
    .pipe(zlib.createGunzip())
    .on('error', deferred.reject)
    .pipe(fs.createWriteStream(dst))
    .on('error', deferred.reject)
    .on('close', deferred.resolve.bind(deferred, 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 moveSingleDirContents(dir) {
    var destDir = path.dirname(dir);

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

        promises = files.map(function (file) {
            var src = path.join(dir, 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, dir);
    });
}

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

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

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

    // TODO: There's an issue here if the src and dst are the same and
    //       The zip name is the same as some of the zip file contents
    //       Maybe create a temp directory inside dst, unzip it there,
    //       unlink zip and then move contents

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

    // Move contents if a single directory was extracted
    if (!opts.keepStructure) {
        promise = promise
        .then(function () {
            return isSingleDir(dst);
        })
        .then(function (singleDir) {
            return singleDir ? moveSingleDirContents(singleDir) : null;
        });
    }

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

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