var core = require('./core'); var fs = require('fs'); var path = require('path'); var caller = require('./caller.js'); var nodeModulesPaths = require('./node-modules-paths.js'); var normalizeOptions = require('./normalize-options.js');

var defaultIsFile = function isFile(file, cb) {

fs.stat(file, function (err, stat) {
    if (!err) {
        return cb(null, stat.isFile() || stat.isFIFO());
    }
    if (err.code === 'ENOENT' || err.code === 'ENOTDIR') return cb(null, false);
    return cb(err);
});

};

var defaultIsDir = function isDirectory(dir, cb) {

fs.stat(dir, function (err, stat) {
    if (!err) {
        return cb(null, stat.isDirectory());
    }
    if (err.code === 'ENOENT' || err.code === 'ENOTDIR') return cb(null, false);
    return cb(err);
});

};

var maybeUnwrapSymlink = function maybeUnwrapSymlink(x, opts, cb) {

if (opts && opts.preserveSymlinks === false) {
    fs.realpath(x, function (realPathErr, realPath) {
        if (realPathErr && realPathErr.code !== 'ENOENT') cb(realPathErr);
        else cb(null, realPathErr ? x : realPath);
    });
} else {
    cb(null, x);
}

};

module.exports = function resolve(x, options, callback) {

var cb = callback;
var opts = options;
if (typeof options === 'function') {
    cb = opts;
    opts = {};
}
if (typeof x !== 'string') {
    var err = new TypeError('Path must be a string.');
    return process.nextTick(function () {
        cb(err);
    });
}

opts = normalizeOptions(x, opts);

var isFile = opts.isFile || defaultIsFile;
var isDirectory = opts.isDirectory || defaultIsDir;
var readFile = opts.readFile || fs.readFile;

var extensions = opts.extensions || ['.js'];
var basedir = opts.basedir || path.dirname(caller());
var parent = opts.filename || basedir;

opts.paths = opts.paths || [];

// ensure that `basedir` is an absolute path at this point, resolving against the process' current working directory
var absoluteStart = path.resolve(basedir);

maybeUnwrapSymlink(
    absoluteStart,
    opts,
    function (err, realStart) {
        if (err) cb(err);
        else init(realStart);
    }
);

var res;
function init(basedir) {
    if ((/^(?:\.\.?(?:\/|$)|\/|([A-Za-z]:)?[/\\])/).test(x)) {
        res = path.resolve(basedir, x);
        if (x === '..' || x.slice(-1) === '/') res += '/';
        if ((/\/$/).test(x) && res === basedir) {
            loadAsDirectory(res, opts.package, onfile);
        } else loadAsFile(res, opts.package, onfile);
    } else loadNodeModules(x, basedir, function (err, n, pkg) {
        if (err) cb(err);
        else if (core[x]) return cb(null, x);
        else if (n) {
            return maybeUnwrapSymlink(n, opts, function (err, realN) {
                if (err) {
                    cb(err);
                } else {
                    cb(null, realN, pkg);
                }
            });
        } else {
            var moduleError = new Error("Cannot find module '" + x + "' from '" + parent + "'");
            moduleError.code = 'MODULE_NOT_FOUND';
            cb(moduleError);
        }
    });
}

function onfile(err, m, pkg) {
    if (err) cb(err);
    else if (m) cb(null, m, pkg);
    else loadAsDirectory(res, function (err, d, pkg) {
        if (err) cb(err);
        else if (d) {
            maybeUnwrapSymlink(d, opts, function (err, realD) {
                if (err) {
                    cb(err);
                } else {
                    cb(null, realD, pkg);
                }
            });
        } else {
            var moduleError = new Error("Cannot find module '" + x + "' from '" + parent + "'");
            moduleError.code = 'MODULE_NOT_FOUND';
            cb(moduleError);
        }
    });
}

function loadAsFile(x, thePackage, callback) {
    var loadAsFilePackage = thePackage;
    var cb = callback;
    if (typeof loadAsFilePackage === 'function') {
        cb = loadAsFilePackage;
        loadAsFilePackage = undefined;
    }

    var exts = [''].concat(extensions);
    load(exts, x, loadAsFilePackage);

    function load(exts, x, loadPackage) {
        if (exts.length === 0) return cb(null, undefined, loadPackage);
        var file = x + exts[0];

        var pkg = loadPackage;
        if (pkg) onpkg(null, pkg);
        else loadpkg(path.dirname(file), onpkg);

        function onpkg(err, pkg_, dir) {
            pkg = pkg_;
            if (err) return cb(err);
            if (dir && pkg && opts.pathFilter) {
                var rfile = path.relative(dir, file);
                var rel = rfile.slice(0, rfile.length - exts[0].length);
                var r = opts.pathFilter(pkg, x, rel);
                if (r) return load(
                    [''].concat(extensions.slice()),
                    path.resolve(dir, r),
                    pkg
                );
            }
            isFile(file, onex);
        }
        function onex(err, ex) {
            if (err) return cb(err);
            if (ex) return cb(null, file, pkg);
            load(exts.slice(1), x, pkg);
        }
    }
}

function loadpkg(dir, cb) {
    if (dir === '' || dir === '/') return cb(null);
    if (process.platform === 'win32' && (/^\w:[/\\]*$/).test(dir)) {
        return cb(null);
    }
    if ((/[/\\]node_modules[/\\]*$/).test(dir)) return cb(null);

    var pkgfile = path.join(dir, 'package.json');
    isFile(pkgfile, function (err, ex) {
        // on err, ex is false
        if (!ex) return loadpkg(path.dirname(dir), cb);

        readFile(pkgfile, function (err, body) {
            if (err) cb(err);
            try { var pkg = JSON.parse(body); } catch (jsonErr) {}

            if (pkg && opts.packageFilter) {
                pkg = opts.packageFilter(pkg, pkgfile);
            }
            cb(null, pkg, dir);
        });
    });
}

function loadAsDirectory(x, loadAsDirectoryPackage, callback) {
    var cb = callback;
    var fpkg = loadAsDirectoryPackage;
    if (typeof fpkg === 'function') {
        cb = fpkg;
        fpkg = opts.package;
    }

    var pkgfile = path.join(x, 'package.json');
    isFile(pkgfile, function (err, ex) {
        if (err) return cb(err);
        if (!ex) return loadAsFile(path.join(x, 'index'), fpkg, cb);

        readFile(pkgfile, function (err, body) {
            if (err) return cb(err);
            try {
                var pkg = JSON.parse(body);
            } catch (jsonErr) {}

            if (opts.packageFilter) {
                pkg = opts.packageFilter(pkg, pkgfile);
            }

            if (pkg.main) {
                if (typeof pkg.main !== 'string') {
                    var mainError = new TypeError('package “' + pkg.name + '” `main` must be a string');
                    mainError.code = 'INVALID_PACKAGE_MAIN';
                    return cb(mainError);
                }
                if (pkg.main === '.' || pkg.main === './') {
                    pkg.main = 'index';
                }
                loadAsFile(path.resolve(x, pkg.main), pkg, function (err, m, pkg) {
                    if (err) return cb(err);
                    if (m) return cb(null, m, pkg);
                    if (!pkg) return loadAsFile(path.join(x, 'index'), pkg, cb);

                    var dir = path.resolve(x, pkg.main);
                    loadAsDirectory(dir, pkg, function (err, n, pkg) {
                        if (err) return cb(err);
                        if (n) return cb(null, n, pkg);
                        loadAsFile(path.join(x, 'index'), pkg, cb);
                    });
                });
                return;
            }

            loadAsFile(path.join(x, '/index'), pkg, cb);
        });
    });
}

function processDirs(cb, dirs) {
    if (dirs.length === 0) return cb(null, undefined);
    var dir = dirs[0];

    isDirectory(dir, isdir);

    function isdir(err, isdir) {
        if (err) return cb(err);
        if (!isdir) return processDirs(cb, dirs.slice(1));
        var file = path.join(dir, x);
        loadAsFile(file, opts.package, onfile);
    }

    function onfile(err, m, pkg) {
        if (err) return cb(err);
        if (m) return cb(null, m, pkg);
        loadAsDirectory(path.join(dir, x), opts.package, ondir);
    }

    function ondir(err, n, pkg) {
        if (err) return cb(err);
        if (n) return cb(null, n, pkg);
        processDirs(cb, dirs.slice(1));
    }
}
function loadNodeModules(x, start, cb) {
    processDirs(cb, nodeModulesPaths(start, opts, x));
}

};