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

  function Modules(server, options) {
    this.server = server
    this.options = options || {}
    this.modules = Object.create(null)
    this.nonRelative = Object.create(null)
    this.knownModules = Object.create(null)
    this.resolvers = []
    this.modNameTests = []
    this.importTests = []
  }

  Modules.prototype = signal.mixin({
    buildWrappingScope: function(parent, origin, node) {
      var scope = new infer.Scope(parent, node)
      scope.origin = origin
      this.signal("wrapScope", scope)
      return scope
    },

    maybeOverride: function(name) {
      if (!this.options.modules || !this.options.modules.hasOwnProperty(name))
        return false
      if (this.modules[name]) return this.modules[name]

      var override = this.options.modules[name]
      if (typeof(override) == "string" && override.charAt(0) == "=")
        return infer.def.parsePath(override.slice(1))

      var scope = this.buildWrappingScope(infer.cx().topScope, name)
      infer.def.load(override, scope)
      return this.modules[name] = scope.exports
    },

    resolveModule: function(name, parentFile) {
      var over = this.maybeOverride(name)
      if (over) return over
      var known = this.knownModules[name]
      if (known) return known
      if (this.options.dontLoad == true ||
          this.options.dontLoad && new RegExp(this.options.dontLoad).test(name) ||
          this.options.load && !new RegExp(this.options.load).test(name))
        return infer.ANull

      var resolved, relative = isRelative(name)
      for (var i = 0; !resolved && i < this.resolvers.length; i++)
        resolved = this.resolvers[i](name, parentFile)
      if (!resolved) resolved = defaultResolver(name, parentFile)
      if (!resolved) return infer.ANull
      if (typeof resolved != "string") {
        if (!relative) this.nonRelative[name] = true
        return resolved
      }

      var known = this.modules[resolved]
      if (known) return known

      if (/\.js$|(?:^\/)[^\.]+$/.test(resolved))
        this.server.addFile(resolved, null, parentFile)
      if (!relative) this.nonRelative[name] = resolved
      return this.modules[resolved] = new infer.AVal
    },

    findIn: function(array, node) {
      for (var i = 0; i < array.length; i++) {
        var name = array[i](node)
        if (name != null) return name
      }
    },

    isModName: function(node) { return this.findIn(this.modNameTests, node) },
    isImport: function(node) { return this.findIn(this.importTests, node) },

    get: function(name) {
      return this.modules[name] || (this.modules[name] = new infer.AVal)
    },

    completeModuleName: function(completions, query, word) {
      function fromObj(obj, useVal) {
        for (var name in obj)
          if (filter(word, name, query))
            tern.addCompletion(query, completions, name, useVal && obj[name])
      }

      fromObj(this.knownModules, true)
      if (this.options.modules) fromObj(this.options.modules, false)

      var pathsSeen = Object.create(null)
      for (var prop in this.nonRelative) {
        var val = this.nonRelative[prop]
        if (val == true || prop.indexOf("/") == -1) {
          if (filter(word, prop, query)) tern.addCompletion(query, completions, prop)
        } else if (prop.indexOf(word) == 0 && word.indexOf("/") > -1) {
          var afterSlash = /.*?\/(.*)/.exec(prop)[1]
          var found = val.indexOf(afterSlash)
          if (found > -1) {
            var dir = val.slice(0, found) + (/.*?\/(.*\/)?/.exec(word)[1] || "")
            if (dir in pathsSeen) continue
            pathsSeen[dir] = true
            this.completeFileName(completions, query, null, word, dir)
          }
        }
      }
    },

    completeFileName: function(completions, query, parentFile, word, _dir) {
      var path = parentFile ? resolvePath(dirName(parentFile), word) : baseName(word)
      for (var prop in this.modules) {
        if (prop != parentFile && filter(path, prop, query)) {
          if (/\.js$/.test(prop)) prop = prop.slice(0, prop.length - 3)
          var added = prop.slice(path.length)
          tern.addCompletion(query, completions, word + added, this.modules[prop])
        }
      }
    },

    getModType: function(node) {
      var modName = this.isModName(node), imp, prop
      if (modName == null && (imp = this.isImport(node))) {
        modName = imp.name
        prop = imp.prop
      }
      if (modName == null) return

      var modType = this.resolveModule(modName, node.sourceFile.name)
      return (prop ? modType.getProp(prop) : modType).getType()
    }
  })

  function resolvePath(parent, sub) {
    if (/^https?:|^\//.test(sub)) return sub
    if (parent && !/\/$/.test(parent)) parent = parent + "/"
    var m
    while (m = /^\.(\.)?\//.exec(sub)) {
      if (m[1] && parent.length > 1) {
        var lastSlash = parent.lastIndexOf("/", parent.length - 2)
        parent = lastSlash == -1 ? "" : parent.slice(0, lastSlash + 1)
      }
      sub = sub.slice(m[0].length)
    }
    return parent + sub
  }

  function dirName(path) {
    var lastSlash = path.lastIndexOf("/")
    if (lastSlash == -1) return ""
    return path.slice(0, lastSlash + 1)
  }
  function baseName(path) {
    var lastSlash = path.lastIndexOf("/")
    if (lastSlash == -1) return path
    else return path.slice(lastSlash + 1)
  }

  function defaultResolver(name, parentFile) {
    if (!/^\.\.?\//.test(name)) return
    var path = resolvePath(dirName(parentFile), name)
    var server = infer.cx().parent
    if (server.findFile(path)) return path
    if (server.findFile(path + ".js")) return path + ".js"
  }

  // Under node, replace completeFileName with a version that actually
  // queries the file system
  if (require) (function() {
    var fs = require("fs"), path = require("path")

    Modules.prototype.completeFileName = function(completions, query, parentFile, word, dir) {
      var pDir = this.server.projectDir
      var endSlash = /\/$/.test(word)
      if (parentFile) {
        var pt = path.resolve(pDir, path.dirname(parentFile), word)
        dir = endSlash ? pt : path.dirname(pt)
      }
      var base = endSlash ? word : path.dirname(word) + "/"
      var filePart = endSlash ? "" : path.basename(word)

      var me = this
      fs.readdirSync(dir).forEach(function(file) {
        if (/^\./.test(file)) return
        if (filter(filePart, file, query)) {
          var projectPath = me.server.normalizeFilename(path.relative(pDir, path.resolve(dir, file)))
          if (projectPath == parentFile) return
          var value = me.modules[projectPath]
          if (/\.js$/.test(file)) file = file.slice(0, file.length - 3)
          tern.addCompletion(query, completions, base + file, value)
        }
      })
    }
  }())

  function isRelative(path) {
    return /^\.\.?\//.test(path)
  }

  function filter(word, string, query) {
    return query.filter === false || !word ||
      (query.caseInsensitive ? string.toLowerCase() : string).indexOf(word) == 0
  }

  function preCondenseReach(state) {
    var mods = infer.cx().parent.mod.modules.modules
    var node = state.roots["!modules"] = new infer.Obj(null)
    for (var name in mods) {
      var mod = mods[name]
      var id = mod.origin || name
      var prop = node.defProp(id.replace(/\./g, "`"))
      prop.origin = mod.origin
      mod.propagate(prop)
    }
  }

  function postLoadDef(data) {
    var cx = infer.cx(), me = cx.parent.mod.modules
    var mods = cx.definitions[data["!name"]]["!modules"]
    if (mods) for (var name in mods.props) {
      var origin = name.replace(/`/g, ".")
      var mod = me.get(origin)
      mod.origin = origin
      mods.props[name].propagate(mod)
    }
    var known = cx.definitions[data["!name"]]["!known_modules"]
    if (known) for (var name in known.props)
      me.knownModules[name.replace(/`/g, ".")] = known.props[name]
  }

  function findTypeAt(_file, _pos, expr, type) {
    if (!expr) return type
    var me = infer.cx().parent.mod.modules
    var modType = me.getModType(expr.node)
    if (!modType) return type

    // The `type` is a value shared for all string literals.
    // We must create a copy before modifying `origin` and `originNode`.
    // Otherwise all string literals would point to the last jump location
    type = Object.create(type)
    type.origin = modType.origin
    type.originNode = modType.originNode
    if (modType.doc) type.doc = modType.doc
    if (modType.url) type.url = modType.url
    return type
  }

  // Complete previously seen module names when completing in strings passed to require
  function findCompletions(file, query) {
    var wordEnd = tern.resolvePos(file, query.end)
    var expr = infer.findExpressionAround(file.ast, null, wordEnd, file.scope)
    if (!expr) return null
    var me = infer.cx().parent.mod.modules

    if (me.isModName(expr.node) != null)
      return findModuleCompletions(me, file, query, expr.node, wordEnd)

    var imp = me.isImport(expr.node)
    if (imp && imp.name && imp.prop != null)
      return findImportCompletions(me, file, query, expr.node, imp, wordEnd)
  }

  function findImportCompletions(me, file, query, node, imp, wordEnd) {
    var completions = []
    var word = node.name.slice(0, wordEnd - node.start)
    if (query.caseInsensitive) word = word.toLowerCase()

    var modType = me.resolveModule(imp.name, node.sourceFile.name).getType()
    if (!modType) return null
    infer.forAllPropertiesOf(modType, function(prop, obj, depth) {
      if (obj == infer.cx().protos.Object) return
      if (query.filter !== false && word &&
          (query.caseInsensitive ? prop.toLowerCase() : prop).indexOf(word) !== 0) return
      tern.addCompletion(query, completions, prop, obj && obj.props[prop], depth)
    })
    return {
      start: tern.outputPos(query, file, node.start),
      end: tern.outputPos(query, file, wordEnd),
      completions: completions,
      isSpecifier: true
    }
  }

  function findModuleCompletions(me, file, query, argNode, wordEnd) {
    if (argNode.type != "Literal" || typeof argNode.value != "string" ||
        argNode.start > wordEnd || argNode.end < wordEnd) return

    var word = argNode.raw.slice(1, wordEnd - argNode.start), quote = argNode.raw.charAt(0)
    if (word && word.charAt(word.length - 1) == quote)
      word = word.slice(0, word.length - 1)
    if (query.caseInsensitive) word = word.toLowerCase()

    var completions = []
    if (isRelative(word)) me.completeFileName(completions, query, file.name, word)
    else me.completeModuleName(completions, query, word)

    if (argNode.end == wordEnd + 1 && file.text.charAt(wordEnd) == quote)
      ++wordEnd
    return {
      start: tern.outputPos(query, file, argNode.start),
      end: tern.outputPos(query, file, wordEnd),
      isProperty: false,
      completions: completions.map(function(rec) {
        var name = typeof rec == "string" ? rec : rec.name
        var string = JSON.stringify(name)
        if (quote == "'") string = quote + string.slice(1, string.length -1).replace(/'/g, "\\'") + quote
        if (typeof rec == "string") return string
        rec.displayName = name
        rec.name = string
        return rec
      })
    }
  }

  tern.registerPlugin("modules", function(server, options) {
    server.mod.modules = new Modules(server, options)

    server.on("beforeLoad", function(file) {
      file.scope = this.mod.modules.buildWrappingScope(file.scope, file.name, file.ast)
    })

    server.on("afterLoad", function(file) {
      var mod = this.mod.modules.get(file.name)
      mod.origin = file.name
      this.mod.modules.signal("getExports", file, mod)
    })

    server.on("reset", function() {
      this.mod.modules.modules = Object.create(null)
    })

    server.on("preCondenseReach", preCondenseReach)
    server.on("postLoadDef", postLoadDef)
    server.on("typeAt", findTypeAt)
    server.on("completion", findCompletions)
  })

  tern.defineQueryType("exports", {
    takesFile: true,
    run: function(server, query, file) {
      function describe(aval) {
        var target = {}, type = aval.getType(false)
        target.type = infer.toString(type, 3)
        var doc = aval.doc || (type && type.doc), url = aval.url || (type && type.url)
        if (doc) target.doc = doc
        if (url) target.url = url
        var span = tern.getSpan(aval) || (type && tern.getSpan(type))
        if (span) tern.storeSpan(server, query, span, target)
        return target
      }

      var mod = server.mod.modules, known = mod && mod.modules[file.name]
      if (!known) return {}
      var resp = describe(known);
      var type = known.getType(false)
      if (type instanceof infer.Obj) {
        var props = resp.props = {}
        for (var prop in type.props)
          props[prop] = describe(type.props[prop])
      }
      return resp
    }
  })
})
