'use strict'

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

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

  var isFile = opts.isFile || defaultIsFile
  var readFile = opts.readFile || fs.readFile

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

  opts.paths = opts.paths || []

  if (/^(?:\.\.?(?:\/|$)|\/|([A-Za-z]:)?[/\\])/.test(x)) {
    var 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 (n) cb(null, n, pkg)
      else if (core[x]) return cb(null, x)
      else {
        var moduleError = new Error("Cannot find module '" + x + "' from '" + basedir + "'")
        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) cb(null, d, pkg)
        else {
          var moduleError = new Error("Cannot find module '" + x + "' from '" + basedir + "'")
          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 (_, 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 (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]

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