(function(mod) {
  if (typeof exports == "object" && typeof module == "object") // CommonJS
    return mod(require("../lib/infer"), require("../lib/tern"), require("./modules"))
  if (typeof define == "function" && define.amd) // AMD
    return define(["../lib/infer", "../lib/tern", "./modules"], mod)
  mod(tern, tern)
})(function(infer, tern) {
  "use strict"

  var WG_DEFAULT_EXPORT = 95

  function initScope(scope) {
    var defs = infer.cx().definitions.commonjs
    defs.require.propagate(scope.defProp("require"))
    var module = new infer.Obj(defs.Module.getProp("prototype").getType())
    module.propagate(scope.defProp("module"))
    var exports = new infer.Obj(true)
    module.origin = exports.origin = scope.origin
    module.originNode = exports.originNode = scope.originNode
    exports.propagate(scope.defProp("exports"))
    var moduleExports = scope.exports = module.defProp("exports")
    exports.propagate(moduleExports, WG_DEFAULT_EXPORT)
  }

  infer.registerFunction("require", function(_self, _args, argNodes) {
    if (!argNodes || !argNodes.length || argNodes[0].type != "Literal" || typeof argNodes[0].value != "string")
      return infer.ANull
    var cx = infer.cx(), server = cx.parent
    var currentFile = argNodes[0].sourceFile.name

    var name = argNodes[0].value
    var resolved = server.mod.modules.resolveModule(name, currentFile)
    return resolved
  })

  function isStaticRequire(node) {
    if (node.type != "CallExpression" || node.callee.type != "Identifier" || node.callee.name != "require") return
    var arg = node.arguments[0]
    if (arg && arg.type == "Literal" && typeof arg.value == "string") return arg.value
  }

  function isModuleName(node) {
    if (node.type != "Literal" || typeof node.value != "string") return

    var call = infer.findExpressionAround(node.sourceFile.ast, null, node.end, null,
                                          function(_, n) { return isStaticRequire(n) != null })
    if (call && call.node.arguments[0] == node) return node.value
  }

  function isImport(node) {
    if (node.type != "Identifier") return
    var decl = infer.findExpressionAround(node.sourceFile.ast, null, node.end, null, "VariableDeclarator"), name
    if (!decl || decl.node.id != node) return
    var init = decl.node.init
    if (init && (name = isStaticRequire(init)) != null)
      return {name: name, prop: null}
    if (init && init.type == "MemberExpression" && !init.computed && (name = isStaticRequire(init.object)) != null)
      return {name: name, prop: init.property.name}
  }

  function hasProps(obj) {
    if (obj) for (var _prop in obj) return true
  }

  tern.registerPlugin("commonjs", function(server) {
    server.loadPlugin("modules")
    server.mod.modules.on("wrapScope", initScope)
    server.mod.modules.on("getExports", function(file, mod) {
      var exports = file.scope.exports
      if (exports.types.length > 1 || hasProps(exports.getObjType()))
        exports.propagate(mod)
    })

    server.mod.modules.modNameTests.push(isModuleName)
    server.mod.modules.importTests.push(isImport)

    server.addDefs(defs)
  })

  var defs = {
    "!name": "commonjs",
    "!define": {
      require: {
        "!type": "fn(id: string) -> !custom:require",
        resolve: {
          "!type": "fn() -> string",
          "!url": "https://nodejs.org/api/globals.html#globals_require_resolve",
          "!doc": "Use the internal require() machinery to look up the location of a module, but rather than loading the module, just return the resolved filename."
        },
        cache: {
          "!url": "https://nodejs.org/api/globals.html#globals_require_cache",
          "!doc": "Modules are cached in this object when they are required. By deleting a key value from this object, the next require will reload the module."
        },
        extensions: {
          "!url": "https://nodejs.org/api/globals.html#globals_require_extensions",
          "!doc": "Instruct require on how to handle certain file extensions."
        },
        "!url": "https://nodejs.org/api/globals.html#globals_require",
        "!doc": "To require modules."
      },
      Module: {
        "!type": "fn()",
        "!url": "https://nodejs.org/api/modules.html",
        "!doc": "Node has a simple module loading system. In Node, files and modules are in one-to-one correspondence.",
        prototype: {
          exports: {
            "!type": "?",
            "!url": "https://nodejs.org/api/modules.html#modules_module_exports",
            "!doc": "The exports object is created by the Module system. Sometimes this is not acceptable, many want their module to be an instance of some class. To do this assign the desired export object to module.exports. For example suppose we were making a module called a.js"
          },
          require: {
            "!type": "require",
            "!url": "https://nodejs.org/api/modules.html#modules_module_require_id",
            "!doc": "The module.require method provides a way to load a module as if require() was called from the original module."
          },
          id: {
            "!type": "string",
            "!url": "https://nodejs.org/api/modules.html#modules_module_id",
            "!doc": "The identifier for the module. Typically this is the fully resolved filename."
          },
          filename: {
            "!type": "string",
            "!url": "https://nodejs.org/api/modules.html#modules_module_filename",
            "!doc": "The fully resolved filename to the module."
          },
          loaded: {
            "!type": "bool",
            "!url": "https://nodejs.org/api/modules.html#modules_module_loaded",
            "!doc": "Whether or not the module is done loading, or is in the process of loading."
          },
          parent: {
            "!type": "+Module",
            "!url": "https://nodejs.org/api/modules.html#modules_module_parent",
            "!doc": "The module that required this one."
          },
          children: {
            "!type": "[+Module]",
            "!url": "https://nodejs.org/api/modules.html#modules_module_children",
            "!doc": "The module objects required by this one."
          }
        }
      },
      module: {}
    },
    module: {
      "!type": "+Module",
      "!url": "https://nodejs.org/api/globals.html#globals_module",
      "!doc": "A reference to the current module. In particular module.exports is the same as the exports object. module isn't actually a global but rather local to each module."
    }
  }
})
