/*  wrench.js
 *
 *  A collection of various utility functions I've found myself in need of
 *  for use with Node.js (http://nodejs.org/). This includes things like:
 *
 *  - Recursively deleting directories in Node.js (Sync, not Async)
 *  - Recursively copying directories in Node.js (Sync, not Async)
 *  - Recursively chmoding a directory structure from Node.js (Sync, not Async)
 *  - Other things that I'll add here as time goes on. Shhhh...
 *
 *  ~ Ryan McGrath (ryan [at] venodesigns.net)
 */

var fs = require("fs"),
    _path = require("path"),
    isWindows = !!process.platform.match(/^win/);

/*  wrench.readdirSyncRecursive("directory_path");
 *
 *  Recursively dives through directories and read the contents of all the
 *  children directories.
 */
exports.readdirSyncRecursive = function(baseDir) {
    baseDir = baseDir.replace(/\/$/, '');

    var readdirSyncRecursive = function(baseDir) {
        var files = [],
            curFiles,
            nextDirs,
            isDir = function(fname){
                return fs.existsSync(_path.join(baseDir, fname)) ? fs.statSync( _path.join(baseDir, fname) ).isDirectory() : false;
            },
            prependBaseDir = function(fname){
                return _path.join(baseDir, fname);
            };

        curFiles = fs.readdirSync(baseDir);
        nextDirs = curFiles.filter(isDir);
        curFiles = curFiles.map(prependBaseDir);

        files = files.concat( curFiles );

        while (nextDirs.length) {
            files = files.concat( readdirSyncRecursive( _path.join(baseDir, nextDirs.shift()) ) );
        }

        return files;
    };

    // convert absolute paths to relative
    var fileList = readdirSyncRecursive(baseDir).map(function(val){
        return _path.relative(baseDir, val);
    });

    return fileList;
};

/*  wrench.readdirRecursive("directory_path", function(error, files) {});
 *
 *  Recursively dives through directories and read the contents of all the
 *  children directories.
 *
 *  Asynchronous, so returns results/error in callback.
 *  Callback receives the of files in currently recursed directory.
 *  When no more directories are left, callback is called with null for all arguments.
 *
 */
exports.readdirRecursive = function(baseDir, fn) {
    baseDir = baseDir.replace(/\/$/, '');

    var waitCount = 0;

    function readdirRecursive(curDir) {
        var prependcurDir = function(fname){
            return _path.join(curDir, fname);
        };

        waitCount++;
        fs.readdir(curDir, function(e, curFiles) {
            if (e) {
                fn(e);
                return;
            }
            waitCount--;

            curFiles = curFiles.map(prependcurDir);

            curFiles.forEach(function(it) {
                waitCount++;

                fs.stat(it, function(e, stat) {
                    waitCount--;

                    if (e) {
                        fn(e);
                    } else {
                        if (stat.isDirectory()) {
                            readdirRecursive(it);
                        }
                    }

                    if (waitCount == 0) {
                        fn(null, null);
                    }
                });
            });

            fn(null, curFiles.map(function(val) {
                // convert absolute paths to relative
                return _path.relative(baseDir, val);
            }));

            if (waitCount == 0) {
                fn(null, null);
            }
        });
    };

    readdirRecursive(baseDir);
};





/*  wrench.rmdirSyncRecursive("directory_path", failSilent);
 *
 *  Recursively dives through directories and obliterates everything about it. This is a
 *  Sync-function, which blocks things until it's done. No idea why anybody would want an
 *  Asynchronous version. :\
 */
exports.rmdirSyncRecursive = function(path, failSilent) {
    var files;

    try {
        files = fs.readdirSync(path);
    } catch (err) {

        if(failSilent) return;
        throw new Error(err.message);
    }

    /*  Loop through and delete everything in the sub-tree after checking it */
    for(var i = 0; i < files.length; i++) {
        var file = _path.join(path, files[i]);
        var currFile = fs.lstatSync(file);

        if(currFile.isDirectory())  {
            // Recursive function back to the beginning
            exports.rmdirSyncRecursive(file);
        } else if(currFile.isSymbolicLink()) {
            // Unlink symlinks
            if (isWindows) {
                fs.chmodSync(file, 666) // Windows needs this unless joyent/node#3006 is resolved..
            }

            fs.unlinkSync(file);
        } else {
            // Assume it's a file - perhaps a try/catch belongs here?
            if (isWindows) {
                fs.chmodSync(file, 666) // Windows needs this unless joyent/node#3006 is resolved..
            }

            fs.unlinkSync(file);
        }
    }

    /*  Now that we know everything in the sub-tree has been deleted, we can delete the main
     directory. Huzzah for the shopkeep. */
    return fs.rmdirSync(path);
};



function isFileIncluded(opts, dir, filename) {

    function isMatch(filter) {
        if (typeof filter === 'function') {
            return filter(filename, dir) === true;
        }
        else {
            // Maintain backwards compatibility and use just the filename
            return filename.match(filter);
        }
    }

    if (opts.include || opts.exclude) {
        if (opts.exclude) {
            if (isMatch(opts.exclude)) {
                return false;
            }
        }

        if (opts.include) {
            if (isMatch(opts.include)) {
                return true;
            }
            else  {
                return false;
            }
        }

        return true;
    }
    else if (opts.filter) {
        var filter = opts.filter;

        if (!opts.whitelist) {
            // if !opts.whitelist is false every file or directory 
            // which does match opts.filter will be ignored
            return isMatch(filter) ? false : true;
        } else {
            // if opts.whitelist is true every file or directory 
            // which doesn't match opts.filter will be ignored
            return !isMatch(filter) ? false : true;
        }
    }

    return true;
}

/*  wrench.copyDirSyncRecursive("directory_to_copy", "new_directory_location", opts);
 *
 *  Recursively dives through a directory and moves all its files to a new location. This is a
 *  Synchronous function, which blocks things until it's done. If you need/want to do this in
 *  an Asynchronous manner, look at wrench.copyDirRecursively() below. Specify forceDelete to force directory overwrite.
 *
 *  Note: Directories should be passed to this function without a trailing slash.
 */
exports.copyDirSyncRecursive = function(sourceDir, newDirLocation, opts) {
    opts = opts || {};

    try {
        if(fs.statSync(newDirLocation).isDirectory()) { 
            if(opts.forceDelete) {
                exports.rmdirSyncRecursive(newDirLocation);
            } else if(!opts.preserveFiles) {
                return new Error('You are trying to copy a directory onto a directory that already exists. Specify forceDelete or preserveFiles in the opts argument to specify desired behavior');
            }
        }
    } catch(e) { }

    /*  Create the directory where all our junk is moving to; read the mode of the source directory and mirror it */
    var checkDir = fs.statSync(sourceDir);
    try {
        fs.mkdirSync(newDirLocation, checkDir.mode);
    } catch (e) {
        //if the directory already exists, that's okay
        if (e.code !== 'EEXIST') throw e;
    }

    var files = fs.readdirSync(sourceDir);
    var hasFilter = opts.filter || opts.include || opts.exclude;
    var preserveFiles = opts.preserveFiles === true;
    var preserveTimestamps = opts.preserveTimestamps === true;

    for(var i = 0; i < files.length; i++) {
        // ignores all files or directories which match the RegExp in opts.filter
        if(typeof opts !== 'undefined') {
            if (hasFilter) {
                if (!isFileIncluded(opts, sourceDir, files[i])) {
                    continue;
                }
            }
            
            if (opts.excludeHiddenUnix && /^\./.test(files[i])) continue;
        }

        var currFile = fs.lstatSync(_path.join(sourceDir, files[i]));

        var fCopyFile = function(srcFile, destFile) {
            if(typeof opts !== 'undefined' && opts.preserveFiles && fs.existsSync(destFile)) return;

            var contents = fs.readFileSync(srcFile);
            fs.writeFileSync(destFile, contents);
            var stat =  fs.lstatSync(srcFile);
            fs.chmodSync(destFile, stat.mode);
            if (preserveTimestamps) {
                fs.utimesSync(destFile, stat.atime, stat.mtime)
            }
        };

        if(currFile.isDirectory()) {
            /*  recursion this thing right on back. */
            exports.copyDirSyncRecursive(_path.join(sourceDir, files[i]), _path.join(newDirLocation, files[i]), opts);
        } else if(currFile.isSymbolicLink()) {
            var symlinkFull = fs.readlinkSync(_path.join(sourceDir, files[i]));
            symlinkFull = _path.resolve(fs.realpathSync(sourceDir), symlinkFull);

            if (typeof opts !== 'undefined' && !opts.inflateSymlinks) {
                fs.symlinkSync(symlinkFull, _path.join(newDirLocation, files[i]));
                continue;
            }

            var tmpCurrFile = fs.lstatSync(symlinkFull);
            if (tmpCurrFile.isDirectory()) {
                exports.copyDirSyncRecursive(symlinkFull, _path.join(newDirLocation, files[i]), opts);
            } else {
                /*  At this point, we've hit a file actually worth copying... so copy it on over. */
                fCopyFile(symlinkFull, _path.join(newDirLocation, files[i]));
            }
        } else {
            /*  At this point, we've hit a file actually worth copying... so copy it on over. */
            fCopyFile(_path.join(sourceDir, files[i]), _path.join(newDirLocation, files[i]));
        }
    }
};

/*  wrench.chmodSyncRecursive("directory", filemode);
 *
 *  Recursively dives through a directory and chmods everything to the desired mode. This is a
 *  Synchronous function, which blocks things until it's done.
 *
 *  Note: Directories should be passed to this function without a trailing slash.
 */
exports.chmodSyncRecursive = function(sourceDir, filemode) {
    var files = fs.readdirSync(sourceDir);

    for(var i = 0; i < files.length; i++) {
        var currFile = fs.lstatSync(_path.join(sourceDir, files[i]));

        if(currFile.isDirectory()) {
            /*  ...and recursion this thing right on back. */
            exports.chmodSyncRecursive(_path.join(sourceDir, files[i]), filemode);
        } else {
            /*  At this point, we've hit a file actually worth copying... so copy it on over. */
            fs.chmod(_path.join(sourceDir, files[i]), filemode);
        }
    }

    /*  Finally, chmod the parent directory */
    fs.chmod(sourceDir, filemode);
};


/*  wrench.chownSyncRecursive("directory", uid, gid);
 *
 *  Recursively dives through a directory and chowns everything to the desired user and group. This is a
 *  Synchronous function, which blocks things until it's done.
 *
 *  Note: Directories should be passed to this function without a trailing slash.
 */
exports.chownSyncRecursive = function(sourceDir, uid, gid) {
    var files = fs.readdirSync(sourceDir);

    for(var i = 0; i < files.length; i++) {
        var currFile = fs.lstatSync(_path.join(sourceDir, files[i]));

        if(currFile.isDirectory()) {
            /*  ...and recursion this thing right on back. */
            exports.chownSyncRecursive(_path.join(sourceDir, files[i]), uid, gid);
        } else {
            /*  At this point, we've hit a file actually worth chowning... so own it. */
            fs.chownSync(_path.join(sourceDir, files[i]), uid, gid);
        }
    }

    /*  Finally, chown the parent directory */
    fs.chownSync(sourceDir, uid, gid);
};



/*  wrench.rmdirRecursive("directory_path", callback);
 *
 *  Recursively dives through directories and obliterates everything about it.
 */
exports.rmdirRecursive = function rmdirRecursive(dir, failSilent, clbk){
    if(clbk === null || typeof clbk == 'undefined')
        clbk = function(err) {};

    fs.readdir(dir, function(err, files) {
        if(err && typeof failSilent === 'boolean' && !failSilent) 
        return clbk(err);

        if(typeof failSilent === 'function')
            clbk = failSilent;
        
        (function rmFile(err){
            if (err) return clbk(err);

            var filename = files.shift();
            if (filename === null || typeof filename == 'undefined')
                return fs.rmdir(dir, clbk);

            var file = dir+'/'+filename;
            fs.lstat(file, function(err, stat){
                if (err) return clbk(err);
                if (stat.isDirectory())
                    rmdirRecursive(file, rmFile);
                else
                    fs.unlink(file, rmFile);
            });
        })();
    });
};

/*  wrench.copyDirRecursive("directory_to_copy", "new_location", {forceDelete: bool}, callback);
 *
 *  Recursively dives through a directory and moves all its files to a new
 *  location. Specify forceDelete to force directory overwrite.
 *
 *  Note: Directories should be passed to this function without a trailing slash.
 */
exports.copyDirRecursive = function copyDirRecursive(srcDir, newDir, opts, clbk) {
    var originalArguments = Array.prototype.slice.apply(arguments);
    srcDir = _path.normalize(srcDir);
    newDir = _path.normalize(newDir);

    fs.stat(newDir, function(err, newDirStat) {
        if(!err) {
            if(typeof opts !== 'undefined' && typeof opts !== 'function' && opts.forceDelete)
                return exports.rmdirRecursive(newDir, function(err) {
                    copyDirRecursive.apply(this, originalArguments);
                });
            else if(typeof opts !== 'undefined' && typeof opts !== 'function' && !opts.preserveFiles) {
              return clbk(new Error('You are trying to copy a directory onto a directory that already exists. Specify forceDelete or preserveFiles in the opts argument to specify desired behavior'));
            }
        }

	if(typeof opts === 'function')
		clbk = opts;

        fs.stat(srcDir, function(err, srcDirStat){
            if (err) return clbk(err);
            fs.mkdir(newDir, srcDirStat.mode, function(err){
                if (err) return clbk(err);
                fs.readdir(srcDir, function(err, files){
                    if (err) return clbk(err);
                    (function copyFiles(err){
                        if (err) return clbk(err);

                        var filename = files.shift();
                        if (filename === null || typeof filename == 'undefined')
                            return clbk(null);

                        var file = srcDir+'/'+filename,
                            newFile = newDir+'/'+filename;

                        fs.stat(file, function(err, fileStat){
                            if (err) return clbk(err);
                            if (fileStat.isDirectory())
                                copyDirRecursive(file, newFile, copyFiles, clbk);
                            else if (fileStat.isSymbolicLink())
                                fs.readlink(file, function(err, link){
                                    if (err) return clbk(err);
                                    fs.symlink(link, newFile, copyFiles);
                                });
                            else
                                fs.readFile(file, function(err, data){
                                    if (err) return clbk(err);
                                    fs.writeFile(newFile, data, copyFiles);
                                });
                        });
                    })();
                });
            });
        });
    });
};

var mkdirSyncRecursive = function(path, mode) {
    var self = this;
    path = _path.normalize(path)

    try {
        fs.mkdirSync(path, mode);
    } catch(err) {
        if(err.code == "ENOENT") {
            var slashIdx = path.lastIndexOf(_path.sep);

            if(slashIdx > 0) {
                var parentPath = path.substring(0, slashIdx);
                mkdirSyncRecursive(parentPath, mode);
                mkdirSyncRecursive(path, mode);
            } else {
                throw err;
            }
        } else if(err.code == "EEXIST") {
            return;
        } else {
            throw err;
        }
    }
};
exports.mkdirSyncRecursive = mkdirSyncRecursive;

exports.LineReader = function(filename, bufferSize) {
    this.bufferSize = bufferSize || 8192;
    this.buffer = "";
    this.fd = fs.openSync(filename, "r");
    this.currentPosition = 0;
};

exports.LineReader.prototype = {
    close: function() {
        return fs.closeSync(this.fd);
    },

    getBufferAndSetCurrentPosition: function(position) {
        var res = fs.readSync(this.fd, this.bufferSize, position, "ascii");

        this.buffer += res[0];
        if(res[1] === 0) {
            this.currentPosition = -1;
        } else {
            this.currentPosition = position + res[1];
        }

        return this.currentPosition;
    },

    hasNextLine: function() {
        while(this.buffer.indexOf('\n') === -1) {
            this.getBufferAndSetCurrentPosition(this.currentPosition);
            if(this.currentPosition === -1) return false;
        }

        if(this.buffer.indexOf("\n") > -1 || this.buffer.length !== 0) return true;
        return false;
    },

    getNextLine: function() {
        var lineEnd = this.buffer.indexOf("\n"),
            result = this.buffer.substring(0, lineEnd != -1 ? lineEnd : this.buffer.length);

        this.buffer = this.buffer.substring(result.length + 1, this.buffer.length);
        return result;
    }
};

// vim: et ts=4 sw=4
