/**
 * @module vow-fs
 * @author Filatov Dmitry <dfilatov@yandex-team.ru>
 * @version 0.3.2
 * @license
 * Dual licensed under the MIT and GPL licenses:
 *   * http://www.opensource.org/licenses/mit-license.php
 *   * http://www.gnu.org/licenses/gpl.html
 */

var vow = require('vow'),
    Queue = require('vow-queue'),
    openFilesQueue = new Queue(),
    fs = require('fs'),
    path = require('path'),
    os = require('os'),
    uuid = require('node-uuid'),
    glob = require('glob'),
    slice = Array.prototype.slice,
    promisify = function(nodeFn) {
        return function() {
            var defer = vow.defer(),
                args = slice.call(arguments);
            args.push(function(err) {
                err? defer.reject(err) : defer.resolve(arguments[1]);
            });
            try {
                nodeFn.apply(fs, args);
            }
            catch(err) {
                defer.reject(err);
            }
            return defer.promise();
        };
    },
    tmpDir = os.tmpdir || os.tmpDir || function() { return '/tmp'; },
    makeDir = promisify(fs.mkdir),
    removeDir = promisify(fs.rmdir),
    lstat = promisify(fs.lstat),
    emfileTimeout = 1,
    emfileFixWrapper = function(method, weight) {
        var wrapper = function() {
                var callArgs = arguments;
                return openFilesQueue.enqueue(function() {
                    return method.apply(vfs, callArgs).then(
                       function(res) {
                           emfileTimeout = Math.max(1, emfileTimeout / 2);
                           return res;
                       },
                       function(err) {
                           if(err.code === 'EMFILE') {
                               emfileTimeout++;
                               return vow.delay(null, emfileTimeout).then(function() {
                                   return wrapper.apply(vfs, callArgs);
                               });
                           }
                           else {
                               throw err;
                           }
                       });
                }, weight);
           };
           return wrapper;
    },
    undef;

var vfs = module.exports = {
    /**
     * Reads file by given path
     * @param {String} path
     * @param {String} [encoding=utf8]
     * @returns {vow:Promise}
     */
    read : emfileFixWrapper(promisify(fs.readFile)),

    /**
     * Writes data to file by given path
     * @param {String} path
     * @param {String|Buffer} data
     * @param {String} [encoding=utf8]
     * @returns {vow:Promise}
     */
    write : emfileFixWrapper(promisify(fs.writeFile)),

    /**
     * Appends data to file by given path
     * @param {String} path
     * @param {String|Buffer} data
     * @param {String} [encoding=utf8]
     * @returns {vow:Promise}
     */
    append : emfileFixWrapper(promisify(fs.appendFile)),

    /**
     * Removes file at given path
     * @param {String} pathToRemove
     * @returns {vow:Promise}
     */
    remove : promisify(fs.unlink),

    /**
     * Copies file from sourcePath to targetPath
     * @param {String} sourcePath
     * @param {String} targetPath
     * @returns {vow:Promise}
     */
    copy : emfileFixWrapper(function(sourcePath, targetPath) {
        return this.isFile(sourcePath).then(function(isFile) {
            if(!isFile) {
                var err = Error();
                err.errno = 28;
                err.code = 'EISDIR';
                err.path = sourcePath;
                throw err;
            }

            var defer = vow.defer(),
                sourceStream = fs.createReadStream(sourcePath),
                errFn = function(err) {
                    defer.reject(err);
                };

            sourceStream
                .on('error', errFn)
                .on('open', function() {
                    var targetStream = fs.createWriteStream(targetPath);
                    sourceStream.pipe(
                        targetStream
                            .on('error', errFn)
                            .on('close', function() {
                                defer.resolve();
                            }));
                });

            return defer.promise();
        });
    }, 2),

    /**
     * Moves from sourcePath to targetPath
     * @param {String} sourcePath
     * @param {String} targetPath
     * @returns {vow:Promise}
     */
    move : promisify(fs.rename),

    /**
     * Extracts fs.Stats about a given path
     * @param {String} path
     * @returns {vow:Promise}
     */
    stat : promisify(fs.stat),

    /**
     * Tests whether or not the given path exists
     * @param {String} path
     * @returns {vow:Promise}
     */
    exists : fs.exists?
        function(path) {
            var defer = vow.defer();
            fs.exists(path, function(exists) {
                defer.resolve(exists);
            });
            return defer.promise();
        } :
        function(path) {
            var defer = vow.defer();
            fs.stat(path, function(err) {
                defer.resolve(!err);
            });
            return defer.promise();
        },

    /**
     * Creates a hard link from the sourcePath to targetPath
     * @param {String} sourcePath
     * @param {String} targetPath
     * @returns {vow:Promise}
     */
    link : promisify(fs.link),

    /**
     * Creates a relative symbolic link from the sourcePath to targetPath
     * @param {String} sourcePath
     * @param {String} targetPath
     * @param {String} [type=file] can be either 'dir', 'file', or 'junction'
     * @returns {vow:Promise}
     */
    symLink : promisify(fs.symlink),

    /**
     * Changes the owner for a given path using Unix user-id and group-id numbers
     * @param {String} path
     * @param uid
     * @param gid
     * @returns {vow:Promise}
     */
    chown : promisify(fs.chown),

    /**
     * Changes the Unix mode for a path. Returns a promise
     * @param {String} path
     * @param mode
     * @returns {vow:Promise}
     */
    chmod : promisify(fs.chmod),

    /**
     * Normalizes a given path to absolute path
     * @param {String} path
     * @returns {vow:Promise}
     */
    absolute : promisify(fs.realpath),

    /**
     * Checks whether the given path is a file
     * @param {String} path
     * @returns {vow:Promise}
     */
    isFile : function(path) {
        return this.stat(path).then(function(stats) {
            return stats.isFile();
        });
    },

    /**
     * Checks whether the given path is a directory
     * @param {String} path
     * @returns {vow:Promise}
     */
    isDir : function(path) {
        return this.stat(path).then(function(stats) {
            return stats.isDirectory();
        });
    },

    /**
     * Checks whether the given path is a socket
     * @param {String} path
     * @returns {vow:Promise}
     */
    isSocket : function(path) {
        return this.stat(path).then(function(stats) {
            return stats.isSocket();
        });
    },

    /**
     * Checks whether the given path is a symbolic link
     * @param {String} path
     * @returns {vow:Promise}
     */
    isSymLink : function(path) {
        return lstat(path).then(function(stats) {
            return stats.isSymbolicLink();
        });
    },

    /**
     * Makes a temporary file
     * @param {Object} options
     * @param {String} [options.prefix]
     * @param {String} [options.dir=os.tmpdir()]
     * @param {String} [options.ext=tmp]
     * @returns {vow:Promise}
     */
    makeTmpFile : function(options) {
        options || (options = {});

        var filePath = path.join(
                options.dir || tmpDir(),
                (options.prefix || '') + uuid.v4() + (options.ext || '.tmp'));

        return vfs.write(filePath, '').then(function() {
            return filePath;
        });
    },

    /**
     * Reads the contents of a directory by given path
     * @param {String} path
     * @returns {vow:Promise}
     */
    listDir : emfileFixWrapper(promisify(fs.readdir)),

    /**
     * Makes a directory at a given path, recursively creating any branches that doesn't exist
     * @param {String} dirPath
     * @param [mode=0777]
     * @param [failIfExist=false]
     * @returns {vow:Promise}
     */
    makeDir : function(dirPath, mode, failIfExist) {
        if(typeof mode === 'boolean') {
            failIfExist = mode;
            mode = undef;
        }

        var dirName = path.dirname(dirPath),
            onFailed = function(e) {
                if(e.code !== 'EEXIST' || failIfExist) {
                    throw e;
                }

                return vfs.isDir(dirPath).then(function(isDir) {
                    if(!isDir) {
                        throw e;
                    }
                });
            };

        return vfs.exists(dirName).then(function(exists) {
            if(exists) {
                return makeDir(dirPath, mode).fail(onFailed);
            }
            else {
                failIfExist = false;
                return vfs.makeDir(dirName, mode).then(function() {
                    return makeDir(dirPath, mode).fail(onFailed);
                });
            }
        });
    },

    /**
     * Removes directory
     * @param {String} dirPath
     * @returns {vow:Promise}
     */
    removeDir : function(dirPath) {
        return vfs.listDir(dirPath)
            .then(function(list) {
                return list.length && vow.all(
                    list.map(function(file) {
                        var fullPath = path.join(dirPath, file);
                        return vfs.isFile(fullPath).then(function(isFile) {
                            return isFile?
                                vfs.remove(fullPath) :
                                vfs.removeDir(fullPath);
                        });
                    }));
            })
            .then(function() {
                return removeDir(dirPath);
            });
    },

    /**
     * Matches files using the patterns, see https://github.com/isaacs/node-glob for details
     * @param {String} pattern to be matched
     * @param {Object} [options] options
     * @returns {vow:Promise}
     */
    glob : promisify(glob),

    options : function(opts) {
        if(typeof opts.openFileLimit !== 'undefined') {
            openFilesQueue.params({ weightLimit : opts.openFileLimit });
        }
    }
};

openFilesQueue.start();
