/*
Copyright (c) 2012, Yahoo! Inc. All rights reserved.
Code licensed under the BSD License:
http://yuilibrary.com/license/
*/
var fs = require('graceful-fs');
var Stack = require('./stack').Stack;
var path = require('path');
var rimraf = require('rimraf');
var mkdirp = require('mkdirp');

var getTree = function(from, options, callback) {
    var stack = new Stack(),
        errors = [],
        results = {};

    options.stats = options.stats || {};
    options.toHash = options.toHash || {};

    fs.readdir(from, stack.add(function(err, dirs) {
        if (!dirs.length) {
            results[from] = true;
            fs.stat(from, stack.add(function(err, stat) {
                /*istanbul ignore next*/
                if (err) {
                    return errors.push(err);
                }
                options.stats[from] = stat;
                options.toHash[from] = path.join(options.to, path.relative(options.from, from));
            }));
        }
        dirs.forEach(function (dir) {
            var base = path.join(from, dir);
            fs.stat(base, stack.add(function(err, stat) {
                options.stats[base] = stat;
                options.toHash[base] = path.join(options.to, path.relative(options.from, base));
                /*istanbul ignore next*/
                if (err) {
                    return errors.push(err);
                }
                if (stat.isDirectory()) {
                    getTree(base, options, stack.add(function(errs, tree) {
                        /*istanbul ignore next*/
                        if (errs && errs.length) {
                            errs.forEach(function(item) {
                                errors.push(item);
                            });
                        }
                        //tree is always an Array
                        tree.forEach(function(item) {
                            results[item] = true;
                        });
                    }));
                } else {
                    results[base] = true;
                }
            }));
        });
    }));

    stack.done(function() {
        callback(errors, Object.keys(results).sort());
    });
};

var filterTree = function (tree, options, callback) {
    var t = tree;
    if (options.filter) {
        if (typeof options.filter === 'function') {
            t = tree.filter(options.filter);
        } else if (options.filter instanceof RegExp) {
            t = tree.filter(function(item) {
                return !options.filter.test(item);
            });
        }
    }
    callback(null, t);
};

var splitTree = function (tree, options, callback) {
    var files = {},
        dirs = {};

    tree.forEach(function(item) {
        var to = options.toHash[item];
        if (options.stats[item] && options.stats[item].isDirectory()) {
            dirs[item] = true;
        } else {
            dirs[path.dirname(item)] = true;
            options.stats[path.dirname(item)] = fs.statSync(path.dirname(item));
            options.toHash[path.dirname(item)] = path.dirname(to);
        }
    });

    tree.forEach(function(item) {
        if (!dirs[item]) {
            files[item] = true;
        }
    });

    callback(Object.keys(dirs).sort(), Object.keys(files).sort());
};


var createDirs = function(dirs, to, options, callback) {
    var stack = new Stack();

    dirs.forEach(function(dir) {
        var stat = options.stats[dir],
            to = options.toHash[dir];

        /*istanbul ignore else*/
        if (to && typeof to === 'string') {
            fs.stat(to, stack.add(function(err, s) {
                if (s && !s.isDirectory()) {
                    /*istanbul ignore next*/
                    err = new Error(to + ' exists and is not a directory, can not create');
                    /*istanbul ignore next*/
                    err.code = 'ENOTDIR';
                    /*istanbul ignore next*/
                    err.errno = 27;
                    options.errors.push(err);
                } else {
                    mkdirp(to, stat.mode, stack.add(function(err) {
                        /*istanbul ignore next*/
                        if (err) {
                            options.errors.push(err);
                        }
                    }));
                }
            }));
        }
    });

    stack.done(function() {
        callback();
    });
};

var copyFile = function(from, to, options, callback) {
    var dir = path.dirname(to);
    mkdirp(dir, function() {
        fs.stat(to, function(statError) {
            var err;
            if(!statError && options.overwrite !== true) {
                /*istanbul ignore next*/
                err = new Error('File '+to+' exists');
                /*istanbul ignore next*/
                err.code = 'EEXIST';
                /*istanbul ignore next*/
                err.errno = 47;
                return callback(err);
            }

            var fromFile = fs.createReadStream(from),
                toFile = fs.createWriteStream(to, {
                    mode: options.stats[from].mode
                }),
                called = false,
                cb = function(e) {
                    /*istanbul ignore next - This catches a hard to trap race condition */
                    if (!called) {
                        callback(e);
                        called = true;
                    }
                },
                /*istanbul ignore next*/
                onError = function (e) {
                    err = e;
                    cb(e);
                };

            fromFile.on('error', onError);
            toFile.on('error', onError);
            toFile.once('finish', function() {
                cb(err);
            });
            fromFile.pipe(toFile);
        });
    });
};

var createFiles = function(files, to, options, callback) {
    var next = process.nextTick,
        complete = 0,
        count = files.length,
        check = function() {
            /*istanbul ignore else - Shouldn't need this if graceful-fs does it's job*/
            if (count === complete) {
                callback();
            }
        },
        copy = function() {
            var from = files.pop(),
                to = options.toHash[from],
                bail;
            if (!from) {
                return check();
            }
            copyFile(from, to, options, function(err) {
                /*istanbul ignore next*/
                //This shouldn't happen with graceful-fs, but just in case
                if (/EMFILE/.test(err)) {
                    bail = true;
                    files.push(from);
                } else if (err) {
                    options.errors.push(err);
                }
                /*istanbul ignore next*/
                if (!bail) {
                    complete++;
                }
                next(copy);
            });
        };

    copy();
};

var confirm = function(files, options, callback) {
    var stack = new Stack(),
        errors = [],
        f = [],
        filtered = files;

    if (options.filter) {
        if (typeof options.filter === 'function') {
            filtered = files.filter(options.filter);
        } else if (options.filter instanceof RegExp) {
            filtered = files.filter(function(item) {
                return !options.filter.test(item);
            });
        }
    }

    /*istanbul ignore else - filtered should be an array, but just in case*/
    if (filtered.length) {
        filtered.forEach(function(file) {
            fs.stat(file, stack.add(function(err, stat) {
                /*istanbul ignore next*/
                if (err) {
                    errors.push(err);
                } else {
                    if (stat && (stat.isFile() || stat.isDirectory())) {
                        f.push(file);
                    }
                }
            }));
        });
    }

    stack.done(function() {
        /*istanbul ignore next */
        callback(((errors.length) ? errors : null), f.sort());
    });
};

var cpr = function(from, to, opts, callback) {
    if (typeof opts === 'function') {
        callback = opts;
        opts = {};
    }

    var options = {},
        proc;

    /*istanbul ignore next - in case a callback isn't provided*/
    callback = callback || function () {};

    Object.keys(opts).forEach(function(key) {
        options[key] = opts[key];
    });

    options.from = from;
    options.to = to;
    options.errors = [];

    proc = function() {
        getTree(options.from, options, function(err, tree) {
            filterTree(tree, options, function(err, t) {
                splitTree(t, options, function(dirs, files) {
                    if (!dirs.length && !files.length) {
                        return callback(new Error('No files to copy'));
                    }
                    createDirs(dirs, to, options, function() {
                        createFiles(files, to, options, function() {
                            var out = [], err;
                            Object.keys(options.toHash).forEach(function(k) {
                                out.push(options.toHash[k]);
                            });
                            if (options.confirm) {
                                confirm(out, options, callback);
                            } else if (!options.errors.length) {
                                callback(null, out.sort());
                            } else {
                                /*istanbul ignore next*/
                                err = new Error('Unable to copy directory' + (out.length ? ' entirely' : ''));
                                /*istanbul ignore next*/
                                err.list = options.errors;
                                /*istanbul ignore next*/
                                callback(err, out.sort());
                            }
                        });
                    });
                });
            });
        });
    };

    fs.stat(options.from, function(err, stat) {
        if (err) {
            return callback(new Error('From should be a file or directory'));
        }
        if (stat && stat.isDirectory()) {
            if (options.deleteFirst) {
                rimraf(to, function() {
                    proc();
                });
            } else {
                proc();

            }
        } else {
            if (stat.isFile()) {
                var dirRegex = new RegExp(path.sep + '$');
                if (dirRegex.test(to)) { // Create directory if has trailing separator
                  to = path.join(to, path.basename(options.from));
                }
                // ensure copyFile() can access cached stat
                options.stats = options.stats || {};
                options.stats[from] = stat;
                return copyFile(options.from, to, options, callback);
            }
            callback(new Error('From should be a file or directory'));
        }
    });
};

//Preserve backward compatibility
cpr.cpr = cpr;
//Export a function
module.exports = cpr;
