/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

;((factory) => { // Module boilerplate :(
  if (typeof(require) === 'function') { // CommonJS
    require("chrome").Cu.import(module.uri, exports);
  }
  else if (~String(this).indexOf('BackstagePass')) { // JSM
    let module = { uri: __URI__, id: "toolkit/loader", exports: Object.create(null) }
    factory(module);
    Object.assign(this, module.exports);
    this.EXPORTED_SYMBOLS = Object.getOwnPropertyNames(module.exports);
  }
  else {
    throw Error("Loading environment is not supported");
  }
})(module => {

'use strict';

module.metadata = {
  "stability": "unstable"
};

const { classes: Cc, Constructor: CC, interfaces: Ci, utils: Cu,
        results: Cr, manager: Cm } = Components;
const systemPrincipal = CC('@mozilla.org/systemprincipal;1', 'nsIPrincipal')();
const { loadSubScript } = Cc['@mozilla.org/moz/jssubscript-loader;1'].
                     getService(Ci.mozIJSSubScriptLoader);
const { notifyObservers } = Cc['@mozilla.org/observer-service;1'].
                        getService(Ci.nsIObserverService);
const { XPCOMUtils } = Cu.import("resource://gre/modules/XPCOMUtils.jsm", {});
const { NetUtil } = Cu.import("resource://gre/modules/NetUtil.jsm", {});
const { join: pathJoin, normalize, dirname } = Cu.import("resource://gre/modules/osfile/ospath_unix.jsm");

XPCOMUtils.defineLazyGetter(this, "XulApp", () => {
  let xulappURI = module.uri.replace("toolkit/loader.js",
                                       "sdk/system/xul-app.jsm");
  return Cu.import(xulappURI, {});
});

// Define some shortcuts.
const bind = Function.call.bind(Function.bind);
const getOwnPropertyDescriptor = Object.getOwnPropertyDescriptor;
const define = Object.defineProperties;
const prototypeOf = Object.getPrototypeOf;
const create = Object.create;
const keys = Object.keys;
const getOwnIdentifiers = x => [...Object.getOwnPropertyNames(x),
                                ...Object.getOwnPropertySymbols(x)];

const NODE_MODULES = ["assert", "buffer_ieee754", "buffer", "child_process", "cluster", "console", "constants", "crypto", "_debugger", "dgram", "dns", "domain", "events", "freelist", "fs", "http", "https", "_linklist", "module", "net", "os", "path", "punycode", "querystring", "readline", "repl", "stream", "string_decoder", "sys", "timers", "tls", "tty", "url", "util", "vm", "zlib"];

const COMPONENT_ERROR = '`Components` is not available in this context.\n' +
  'Functionality provided by Components may be available in an SDK\n' +
  'module: https://developer.mozilla.org/en-US/Add-ons/SDK \n\n' +
  'However, if you still need to import Components, you may use the\n' +
  '`chrome` module\'s properties for shortcuts to Component properties:\n\n' +
  'Shortcuts: \n' +
  '    Cc = Components' + '.classes \n' +
  '    Ci = Components' + '.interfaces \n' +
  '    Cu = Components' + '.utils \n' +
  '    CC = Components' + '.Constructor \n' +
  'Example: \n' +
  '    let { Cc, Ci } = require(\'chrome\');\n';

// Workaround for bug 674195. Freezing objects from other compartments fail,
// so we use `Object.freeze` from the same component instead.
function freeze(object) {
  if (prototypeOf(object) === null) {
      Object.freeze(object);
  }
  else {
    prototypeOf(prototypeOf(object.isPrototypeOf)).
      constructor. // `Object` from the owner compartment.
      freeze(object);
  }
  return object;
}

// Returns map of given `object`-s own property descriptors.
const descriptor = iced(function descriptor(object) {
  let value = {};
  getOwnIdentifiers(object).forEach(function(name) {
    value[name] = getOwnPropertyDescriptor(object, name)
  });
  return value;
});
Loader.descriptor = descriptor;

// Freeze important built-ins so they can't be used by untrusted code as a
// message passing channel.
freeze(Object);
freeze(Object.prototype);
freeze(Function);
freeze(Function.prototype);
freeze(Array);
freeze(Array.prototype);
freeze(String);
freeze(String.prototype);

// This function takes `f` function sets it's `prototype` to undefined and
// freezes it. We need to do this kind of deep freeze with all the exposed
// functions so that untrusted code won't be able to use them a message
// passing channel.
function iced(f) {
  if (!Object.isFrozen(f)) {
    f.prototype = undefined;
  }
  return freeze(f);
}

// Defines own properties of given `properties` object on the given
// target object overriding any existing property with a conflicting name.
// Returns `target` object. Note we only export this function because it's
// useful during loader bootstrap when other util modules can't be used &
// thats only case where this export should be used.
const override = iced(function override(target, source) {
  let properties = descriptor(target)
  let extension = descriptor(source || {})
  getOwnIdentifiers(extension).forEach(function(name) {
    properties[name] = extension[name];
  });
  return define({}, properties);
});
Loader.override = override;

function sourceURI(uri) { return String(uri).split(" -> ").pop(); }
Loader.sourceURI = iced(sourceURI);

function isntLoaderFrame(frame) { return frame.fileName !== module.uri }

function parseURI(uri) { return String(uri).split(" -> ").pop(); }
Loader.parseURI = parseURI;

function parseStack(stack) {
  let lines = String(stack).split("\n");
  return lines.reduce(function(frames, line) {
    if (line) {
      let atIndex = line.indexOf("@");
      let columnIndex = line.lastIndexOf(":");
      let lineIndex = line.lastIndexOf(":", columnIndex - 1);
      let fileName = parseURI(line.slice(atIndex + 1, lineIndex));
      let lineNumber = parseInt(line.slice(lineIndex + 1, columnIndex));
      let columnNumber = parseInt(line.slice(columnIndex + 1));
      let name = line.slice(0, atIndex).split("(").shift();
      frames.unshift({
        fileName: fileName,
        name: name,
        lineNumber: lineNumber,
        columnNumber: columnNumber
      });
    }
    return frames;
  }, []);
}
Loader.parseStack = parseStack;

function serializeStack(frames) {
  return frames.reduce(function(stack, frame) {
    return frame.name + "@" +
           frame.fileName + ":" +
           frame.lineNumber + ":" +
           frame.columnNumber + "\n" +
           stack;
  }, "");
}
Loader.serializeStack = serializeStack;

function readURI(uri) {
  let stream = NetUtil.newChannel2(uri,
                                   'UTF-8',
                                   null,
                                   null,      // aLoadingNode
                                   systemPrincipal,
                                   null,      // aTriggeringPrincipal
                                   Ci.nsILoadInfo.SEC_NORMAL,
                                   Ci.nsIContentPolicy.TYPE_OTHER).open();
  let count = stream.available();
  let data = NetUtil.readInputStreamToString(stream, count, {
    charset: 'UTF-8'
  });

  stream.close();

  return data;
}

// Combines all arguments into a resolved, normalized path
function join (...paths) {
  let resolved = normalize(pathJoin(...paths))
  // OS.File `normalize` strips out the second slash in
  // `resource://` or `chrome://`, and third slash in
  // `file:///`, so we work around this
  resolved = resolved.replace(/^resource\:\/([^\/])/, 'resource://$1');
  resolved = resolved.replace(/^file\:\/([^\/])/, 'file:///$1');
  resolved = resolved.replace(/^chrome\:\/([^\/])/, 'chrome://$1');
  return resolved;
}
Loader.join = join;

// Function takes set of options and returns a JS sandbox. Function may be
// passed set of options:
//  - `name`: A string value which identifies the sandbox in about:memory. Will
//    throw exception if omitted.
// - `principal`: String URI or `nsIPrincipal` for the sandbox. Defaults to
//    system principal.
// - `prototype`: Ancestor for the sandbox that will be created. Defaults to
//    `{}`.
// - `wantXrays`: A Boolean value indicating whether code outside the sandbox
//    wants X-ray vision with respect to objects inside the sandbox. Defaults
//    to `true`.
// - `sandbox`: A sandbox to share JS compartment with. If omitted new
//    compartment will be created.
// - `metadata`: A metadata object associated with the sandbox. It should
//    be JSON-serializable.
// For more details see:
// https://developer.mozilla.org/en/Components.utils.Sandbox
const Sandbox = iced(function Sandbox(options) {
  // Normalize options and rename to match `Cu.Sandbox` expectations.
  options = {
    // Do not expose `Components` if you really need them (bad idea!) you
    // still can expose via prototype.
    wantComponents: false,
    sandboxName: options.name,
    principal: 'principal' in options ? options.principal : systemPrincipal,
    wantXrays: 'wantXrays' in options ? options.wantXrays : true,
    wantGlobalProperties: 'wantGlobalProperties' in options ?
                          options.wantGlobalProperties : [],
    sandboxPrototype: 'prototype' in options ? options.prototype : {},
    invisibleToDebugger: 'invisibleToDebugger' in options ?
                         options.invisibleToDebugger : false,
    metadata: 'metadata' in options ? options.metadata : {}
  };

  if (options.metadata && options.metadata.addonID) {
    options.addonId = options.metadata.addonID;
  }

  let sandbox = Cu.Sandbox(options.principal, options);

  // Each sandbox at creation gets set of own properties that will be shadowing
  // ones from it's prototype. We override delete such `sandbox` properties
  // to avoid shadowing.
  delete sandbox.Iterator;
  delete sandbox.Components;
  delete sandbox.importFunction;
  delete sandbox.debug;

  return sandbox;
});
Loader.Sandbox = Sandbox;

// Evaluates code from the given `uri` into given `sandbox`. If
// `options.source` is passed, then that code is evaluated instead.
// Optionally following options may be given:
// - `options.encoding`: Source encoding, defaults to 'UTF-8'.
// - `options.line`: Line number to start count from for stack traces.
//    Defaults to 1.
// - `options.version`: Version of JS used, defaults to '1.8'.
const evaluate = iced(function evaluate(sandbox, uri, options) {
  let { source, line, version, encoding } = override({
    encoding: 'UTF-8',
    line: 1,
    version: '1.8',
    source: null
  }, options);

  return source ? Cu.evalInSandbox(source, sandbox, version, uri, line)
                : loadSubScript(uri, sandbox, encoding);
});
Loader.evaluate = evaluate;

// Populates `exports` of the given CommonJS `module` object, in the context
// of the given `loader` by evaluating code associated with it.
const load = iced(function load(loader, module) {
  let { sandboxes, globals } = loader;
  let require = Require(loader, module);

  // We expose set of properties defined by `CommonJS` specification via
  // prototype of the sandbox. Also globals are deeper in the prototype
  // chain so that each module has access to them as well.
  let descriptors = descriptor({
    require: require,
    module: module,
    exports: module.exports,
    get Components() {
      // Expose `Components` property to throw error on usage with
      // additional information
      throw new ReferenceError(COMPONENT_ERROR);
    }
  });

  let sandbox;
  if (loader.sharedGlobalSandbox &&
      loader.sharedGlobalBlacklist.indexOf(module.id) == -1) {
    // Create a new object in this sandbox, that will be used as
    // the scope object for this particular module
    sandbox = new loader.sharedGlobalSandbox.Object();
    // Inject all expected globals in the scope object
    getOwnIdentifiers(globals).forEach(function(name) {
      descriptors[name] = getOwnPropertyDescriptor(globals, name)
    });
    define(sandbox, descriptors);
  }
  else {
    sandbox = Sandbox({
      name: module.uri,
      prototype: create(globals, descriptors),
      wantXrays: false,
      wantGlobalProperties: module.id == "sdk/indexed-db" ? ["indexedDB"] : [],
      invisibleToDebugger: loader.invisibleToDebugger,
      metadata: {
        addonID: loader.id,
        URI: module.uri
      }
    });
  }
  sandboxes[module.uri] = sandbox;

  try {
    evaluate(sandbox, module.uri);
  }
  catch (error) {
    let { message, fileName, lineNumber } = error;
    let stack = error.stack || Error().stack;
    let frames = parseStack(stack).filter(isntLoaderFrame);
    let toString = String(error);
    let file = sourceURI(fileName);

    // Note that `String(error)` where error is from subscript loader does
    // not puts `:` after `"Error"` unlike regular errors thrown by JS code.
    // If there is a JS stack then this error has already been handled by an
    // inner module load.
    if (/^Error opening input stream/.test(String(error))) {
      let caller = frames.slice(0).pop();
      fileName = caller.fileName;
      lineNumber = caller.lineNumber;
      message = "Module `" + module.id + "` is not found at " + module.uri;
      toString = message;
    }
    // Workaround for a Bug 910653. Errors thrown by subscript loader
    // do not include `stack` field and above created error won't have
    // fileName or lineNumber of the module being loaded, so we ensure
    // it does.
    else if (frames[frames.length - 1].fileName !== file) {
      frames.push({ fileName: file, lineNumber: lineNumber, name: "" });
    }

    let prototype = typeof(error) === "object" ? error.constructor.prototype :
                    Error.prototype;

    throw create(prototype, {
      message: { value: message, writable: true, configurable: true },
      fileName: { value: fileName, writable: true, configurable: true },
      lineNumber: { value: lineNumber, writable: true, configurable: true },
      stack: { value: serializeStack(frames), writable: true, configurable: true },
      toString: { value: function() toString, writable: true, configurable: true },
    });
  }

  if (loader.checkCompatibility) {
    let err = XulApp.incompatibility(module);
    if (err) {
      throw err;
    }
  }

  if (module.exports && typeof(module.exports) === 'object')
    freeze(module.exports);

  return module;
});
Loader.load = load;

// Utility function to normalize module `uri`s so they have `.js` extension.
function normalizeExt (uri) {
  return isJSURI(uri) ? uri :
         isJSONURI(uri) ? uri :
         isJSMURI(uri) ? uri :
         uri + '.js';
}

// Strips `rootURI` from `string` -- used to remove absolute resourceURI
// from a relative path
function stripBase (rootURI, string) {
  return string.replace(rootURI, './');
}

// Utility function to join paths. In common case `base` is a
// `requirer.uri` but in some cases it may be `baseURI`. In order to
// avoid complexity we require `baseURI` with a trailing `/`.
const resolve = iced(function resolve(id, base) {
  if (!isRelative(id)) return id;
  let basePaths = base.split('/');
  // Pop the last element in the `base`, because it is from a
  // relative file
  // '../mod.js' from '/path/to/file.js' should resolve to '/path/mod.js'
  basePaths.pop();
  if (!basePaths.length)
    return normalize(id);
  let resolved = join(basePaths.join('/'), id);

  // Joining and normalizing removes the './' from relative files.
  // We need to ensure the resolution still has the root
  if (isRelative(base))
    resolved = './' + resolved;

  return resolved;
});
Loader.resolve = resolve;

// Node-style module lookup
// Takes an id and path and attempts to load a file using node's resolving
// algorithm.
// `id` should already be resolved relatively at this point.
// http://nodejs.org/api/modules.html#modules_all_together
const nodeResolve = iced(function nodeResolve(id, requirer, { rootURI }) {
  // Resolve again
  id = Loader.resolve(id, requirer);

  // we assume that extensions are correct, i.e., a directory doesnt't have '.js'
  // and a js file isn't named 'file.json.js'
  let fullId = join(rootURI, id);
  let resolvedPath;

  if ((resolvedPath = loadAsFile(fullId)))
    return stripBase(rootURI, resolvedPath);

  if ((resolvedPath = loadAsDirectory(fullId)))
    return stripBase(rootURI, resolvedPath);

  // If manifest has dependencies, attempt to look up node modules
  // in the `dependencies` list
  let dirs = getNodeModulePaths(dirname(join(rootURI, requirer))).map(dir => join(dir, id));
  for (let i = 0; i < dirs.length; i++) {
    if ((resolvedPath = loadAsFile(dirs[i])))
      return stripBase(rootURI, resolvedPath);

    if ((resolvedPath = loadAsDirectory(dirs[i])))
      return stripBase(rootURI, resolvedPath);
  }

  // We would not find lookup for things like `sdk/tabs`, as that's part of
  // the alias mapping. If during `generateMap`, the runtime lookup resolves
  // with `resolveURI` -- if during runtime, then `resolve` will throw.
  return void 0;
});
Loader.nodeResolve = nodeResolve;

// Attempts to load `path` and then `path.js`
// Returns `path` with valid file, or `undefined` otherwise
function loadAsFile (path) {
  let found;

  // As per node's loader spec,
  // we first should try and load 'path' (with no extension)
  // before trying 'path.js'. We will not support this feature
  // due to performance, but may add it if necessary for adoption.
  try {
    // Append '.js' to path name unless it's another support filetype
    path = normalizeExt(path);
    readURI(path);
    found = path;
  } catch (e) {}

  return found;
}

// Attempts to load `path/package.json`'s `main` entry,
// followed by `path/index.js`, or `undefined` otherwise
function loadAsDirectory (path) {
  try {
    // If `path/package.json` exists, parse the `main` entry
    // and attempt to load that
    let main = getManifestMain(JSON.parse(readURI(path + '/package.json')));
    if (main != null) {
      let tmpPath = join(path, main);
      let found = loadAsFile(tmpPath);
      if (found)
        return found
    }
    try {
      let tmpPath = path + '/index.js';
      readURI(tmpPath);
      return tmpPath;
    } catch (e) {}
  } catch (e) {
    try {
      let tmpPath = path + '/index.js';
      readURI(tmpPath);
      return tmpPath;
    } catch (e) {}
  }
  return void 0;
}

// From `resolve` module
// https://github.com/substack/node-resolve/blob/master/lib/node-modules-paths.js
function getNodeModulePaths (start) {
  // Configurable in node -- do we need this to be configurable?
  let moduleDir = 'node_modules';

  let parts = start.split('/');
  let dirs = [];
  for (let i = parts.length - 1; i >= 0; i--) {
    if (parts[i] === moduleDir) continue;
    let dir = join(parts.slice(0, i + 1).join('/'), moduleDir);
    dirs.push(dir);
  }
  return dirs;
}


function addTrailingSlash (path) {
  return !path ? null : !path.endsWith('/') ? path + '/' : path;
}

// Utility function to determine of module id `name` is a built in
// module in node (fs, path, etc.);
function isNodeModule (name) {
  return !!~NODE_MODULES.indexOf(name);
}

// Make mapping array that is sorted from longest path to shortest path
// to allow overlays. Used by `resolveURI`, returns an array
function sortPaths (paths) {
  return keys(paths).
    sort(function(a, b) { return b.length - a.length }).
    map(function(path) { return [ path, paths[path] ] });
}

const resolveURI = iced(function resolveURI(id, mapping) {
  let count = mapping.length, index = 0;

  // Do not resolve if already a resource URI
  if (isAbsoluteURI(id)) return normalizeExt(id);

  while (index < count) {
    let [ path, uri ] = mapping[index ++];
    if (id.indexOf(path) === 0)
      return normalizeExt(id.replace(path, uri));
  }
  return void 0; // otherwise we raise a warning, see bug 910304
});
Loader.resolveURI = resolveURI;

// Creates version of `require` that will be exposed to the given `module`
// in the context of the given `loader`. Each module gets own limited copy
// of `require` that is allowed to load only a modules that are associated
// with it during link time.
const Require = iced(function Require(loader, requirer) {
  let {
    modules, mapping, resolve: loaderResolve, load, manifest, rootURI, isNative, requireMap
  } = loader;

  function require(id) {
    if (!id) // Throw if `id` is not passed.
      throw Error('you must provide a module name when calling require() from '
                  + requirer.id, requirer.uri);

    let { uri, requirement } = getRequirements(id);
    let module = null;
    // If module is already cached by loader then just use it.
    if (uri in modules) {
      module = modules[uri];
    }
    else if (isJSMURI(uri)) {
      module = modules[uri] = Module(requirement, uri);
      module.exports = Cu.import(uri, {});
      freeze(module);
    }
    else if (isJSONURI(uri)) {
      let data;

      // First attempt to load and parse json uri
      // ex: `test.json`
      // If that doesn't exist, check for `test.json.js`
      // for node parity
      try {
        data = JSON.parse(readURI(uri));
        module = modules[uri] = Module(requirement, uri);
        module.exports = data;
        freeze(module);
      }
      catch (err) {
        // If error thrown from JSON parsing, throw that, do not
        // attempt to find .json.js file
        if (err && /JSON\.parse/.test(err.message))
          throw err;
        uri = uri + '.js';
      }
    }
    // If not yet cached, load and cache it.
    // We also freeze module to prevent it from further changes
    // at runtime.
    if (!(uri in modules)) {
      // Many of the loader's functionalities are dependent
      // on modules[uri] being set before loading, so we set it and
      // remove it if we have any errors.
      module = modules[uri] = Module(requirement, uri);
      try {
        freeze(load(loader, module));
      }
      catch (e) {
        // Clear out modules cache so we can throw on a second invalid require
        delete modules[uri];
        // Also clear out the Sandbox that was created
        delete loader.sandboxes[uri];
        throw e;
      }
    }

    return module.exports;
  }

  // Resolution function taking a module name/path and
  // returning a resourceURI and a `requirement` used by the loader.
  // Used by both `require` and `require.resolve`.
  function getRequirements(id) {
    if (!id) // Throw if `id` is not passed.
      throw Error('you must provide a module name when calling require() from '
                  + requirer.id, requirer.uri);

    let requirement;
    let uri;

    // TODO should get native Firefox modules before doing node-style lookups
    // to save on loading time
    if (isNative) {
      // If a requireMap is available from `generateMap`, use that to
      // immediately resolve the node-style mapping.
      if (requireMap && requireMap[requirer.id])
        requirement = requireMap[requirer.id][id];

      // For native modules, we want to check if it's a module specified
      // in 'modules', like `chrome`, or `@loader` -- if it exists,
      // just set the uri to skip resolution
      if (!requirement && modules[id])
        uri = requirement = id;

      // If no requireMap was provided, or resolution not found in
      // the requireMap, and not a npm dependency, attempt a runtime lookup
      if (!requirement && !isNodeModule(id)) {
        // If `isNative` defined, this is using the new, native-style
        // loader, not cuddlefish, so lets resolve using node's algorithm
        // and get back a path that needs to be resolved via paths mapping
        // in `resolveURI`
        requirement = loaderResolve(id, requirer.id, {
          manifest: manifest,
          rootURI: rootURI
        });
      }

      // If not found in the map, not a node module, and wasn't able to be
      // looked up, it's something
      // found in the paths most likely, like `sdk/tabs`, which should
      // be resolved relatively if needed using traditional resolve
      if (!requirement) {
        requirement = isRelative(id) ? Loader.resolve(id, requirer.id) : id;
      }
    } else {
      // Resolve `id` to its requirer if it's relative.
      requirement = requirer ? loaderResolve(id, requirer.id) : id;
    }

    // Resolves `uri` of module using loaders resolve function.
    uri = uri || resolveURI(requirement, mapping);

    if (!uri) // Throw if `uri` can not be resolved.
      throw Error('Module: Can not resolve "' + id + '" module required by ' +
                  requirer.id + ' located at ' + requirer.uri, requirer.uri);

    return { uri: uri, requirement: requirement };
  }

  // Expose the `resolve` function for this `Require` instance
  require.resolve = function resolve(id) {
    let { uri } = getRequirements(id);
    return uri;
  }

  // Make `require.main === module` evaluate to true in main module scope.
  require.main = loader.main === requirer ? requirer : undefined;
  return iced(require);
});
Loader.Require = Require;

const main = iced(function main(loader, id) {
  // If no main entry provided, and native loader is used,
  // read the entry in the manifest
  if (!id && loader.isNative)
    id = getManifestMain(loader.manifest);
  let uri = resolveURI(id, loader.mapping);
  let module = loader.main = loader.modules[uri] = Module(id, uri);
  return loader.load(loader, module).exports;
});
Loader.main = main;

// Makes module object that is made available to CommonJS modules when they
// are evaluated, along with `exports` and `require`.
const Module = iced(function Module(id, uri) {
  return create(null, {
    id: { enumerable: true, value: id },
    exports: { enumerable: true, writable: true, value: create(null) },
    uri: { value: uri }
  });
});
Loader.Module = Module;

// Takes `loader`, and unload `reason` string and notifies all observers that
// they should cleanup after them-self.
const unload = iced(function unload(loader, reason) {
  // subject is a unique object created per loader instance.
  // This allows any code to cleanup on loader unload regardless of how
  // it was loaded. To handle unload for specific loader subject may be
  // asserted against loader.destructor or require('@loader/unload')
  // Note: We don not destroy loader's module cache or sandboxes map as
  // some modules may do cleanup in subsequent turns of event loop. Destroying
  // cache may cause module identity problems in such cases.
  let subject = { wrappedJSObject: loader.destructor };
  notifyObservers(subject, 'sdk:loader:destroy', reason);
});
Loader.unload = unload;

// Function makes new loader that can be used to load CommonJS modules
// described by a given `options.manifest`. Loader takes following options:
// - `globals`: Optional map of globals, that all module scopes will inherit
//   from. Map is also exposed under `globals` property of the returned loader
//   so it can be extended further later. Defaults to `{}`.
// - `modules` Optional map of built-in module exports mapped by module id.
//   These modules will incorporated into module cache. Each module will be
//   frozen.
// - `resolve` Optional module `id` resolution function. If given it will be
//   used to resolve module URIs, by calling it with require term, requirer
//   module object (that has `uri` property) and `baseURI` of the loader.
//   If `resolve` does not returns `uri` string exception will be thrown by
//   an associated `require` call.
function Loader(options) {
  let {
    modules, globals, resolve, paths, rootURI, manifest, requireMap, isNative,
    metadata, sharedGlobal, sharedGlobalBlacklist, checkCompatibility
  } = override({
    paths: {},
    modules: {},
    globals: {
      get console() {
        // Import Console.jsm from here to prevent loading it until someone uses it
        let { ConsoleAPI } = Cu.import("resource://gre/modules/devtools/Console.jsm");
        let console = new ConsoleAPI({
          consoleID: options.id ? "addon/" + options.id : ""
        });
        Object.defineProperty(this, "console", { value: console });
        return this.console;
      }
    },
    checkCompatibility: false,
    resolve: options.isNative ?
      // Make the returned resolve function have the same signature
      (id, requirer) => Loader.nodeResolve(id, requirer, { rootURI: rootURI }) :
      Loader.resolve,
    sharedGlobalBlacklist: ["sdk/indexed-db"]
  }, options);

  // We create an identity object that will be dispatched on an unload
  // event as subject. This way unload listeners will be able to assert
  // which loader is unloaded. Please note that we intentionally don't
  // use `loader` as subject to prevent a loader access leakage through
  // observer notifications.
  let destructor = freeze(create(null));

  let mapping = sortPaths(paths);

  // Define pseudo modules.
  modules = override({
    '@loader/unload': destructor,
    '@loader/options': options,
    'chrome': { Cc: Cc, Ci: Ci, Cu: Cu, Cr: Cr, Cm: Cm,
                CC: bind(CC, Components), components: Components,
                // `ChromeWorker` has to be inject in loader global scope.
                // It is done by bootstrap.js:loadSandbox for the SDK.
                ChromeWorker: ChromeWorker
    }
  }, modules);

  modules = keys(modules).reduce(function(result, id) {
    // We resolve `uri` from `id` since modules are cached by `uri`.
    let uri = resolveURI(id, mapping);
    // In native loader, the mapping will not contain values for
    // pseudomodules -- store them as their ID rather than the URI
    if (isNative && !uri)
      uri = id;
    let module = Module(id, uri);
    module.exports = freeze(modules[id]);
    result[uri] = freeze(module);
    return result;
  }, {});

  let sharedGlobalSandbox;
  if (sharedGlobal) {
    // Create the unique sandbox we will be using for all modules,
    // so that we prevent creating a new comportment per module.
    // The side effect is that all modules will share the same
    // global objects.
    sharedGlobalSandbox = Sandbox({
      name: "Addon-SDK",
      wantXrays: false,
      wantGlobalProperties: [],
      invisibleToDebugger: options.invisibleToDebugger || false,
      metadata: {
        addonID: options.id,
        URI: "Addon-SDK"
      }
    });
  }

  // Loader object is just a representation of a environment
  // state. We freeze it and mark make it's properties non-enumerable
  // as they are pure implementation detail that no one should rely upon.
  let returnObj = {
    destructor: { enumerable: false, value: destructor },
    globals: { enumerable: false, value: globals },
    mapping: { enumerable: false, value: mapping },
    // Map of module objects indexed by module URIs.
    modules: { enumerable: false, value: modules },
    metadata: { enumerable: false, value: metadata },
    sharedGlobalSandbox: { enumerable: false, value: sharedGlobalSandbox },
    sharedGlobalBlacklist: { enumerable: false, value: sharedGlobalBlacklist },
    // Map of module sandboxes indexed by module URIs.
    sandboxes: { enumerable: false, value: {} },
    resolve: { enumerable: false, value: resolve },
    // ID of the addon, if provided.
    id: { enumerable: false, value: options.id },
    // Whether the modules loaded should be ignored by the debugger
    invisibleToDebugger: { enumerable: false,
                           value: options.invisibleToDebugger || false },
    load: { enumerable: false, value: options.load || load },
    checkCompatibility: { enumerable: false, value: checkCompatibility },
    // Main (entry point) module, it can be set only once, since loader
    // instance can have only one main module.
    main: new function() {
      let main;
      return {
        enumerable: false,
        get: function() { return main; },
        // Only set main if it has not being set yet!
        set: function(module) { main = main || module; }
      }
    }
  };

  if (isNative) {
    returnObj.isNative = { enumerable: false, value: true };
    returnObj.manifest = { enumerable: false, value: manifest };
    returnObj.requireMap = { enumerable: false, value: requireMap };
    returnObj.rootURI = { enumerable: false, value: addTrailingSlash(rootURI) };
  }

  return freeze(create(null, returnObj));
};
Loader.Loader = Loader;

let isJSONURI = uri => uri.substr(-5) === '.json';
let isJSMURI = uri => uri.substr(-4) === '.jsm';
let isJSURI = uri => uri.substr(-3) === '.js';
let isAbsoluteURI = uri => uri.indexOf("resource://") >= 0 ||
                           uri.indexOf("chrome://") >= 0 ||
                           uri.indexOf("file://") >= 0
let isRelative = id => id[0] === '.'

const generateMap = iced(function generateMap(options, callback) {
  let { rootURI, resolve, paths } = override({
    paths: {},
    resolve: Loader.nodeResolve
  }, options);

  rootURI = addTrailingSlash(rootURI);

  let manifest;
  let manifestURI = join(rootURI, 'package.json');

  if (rootURI)
    manifest = JSON.parse(readURI(manifestURI));
  else
    throw new Error('No `rootURI` given to generate map');

  let main = getManifestMain(manifest);

  findAllModuleIncludes(main, {
    resolve: resolve,
    manifest: manifest,
    rootURI: rootURI
  }, {}, callback);

});
Loader.generateMap = generateMap;

// Default `main` entry to './index.js' and ensure is relative,
// since node allows 'lib/index.js' without relative `./`
function getManifestMain (manifest) {
  let main = manifest.main || './index.js';
  return isRelative(main) ? main : './' + main;
}

function findAllModuleIncludes (uri, options, results, callback) {
  let { resolve, manifest, rootURI } = options;
  results = results || {};

  // Abort if JSON or JSM
  if (isJSONURI(uri) || isJSMURI(uri)) {
    callback(results);
    return;
  }

  findModuleIncludes(join(rootURI, uri), modules => {
    // If no modules are included in the file, just call callback immediately
    if (!modules.length) {
      callback(results);
      return;
    }

    results[uri] = modules.reduce((agg, mod) => {
      let resolved = resolve(mod, uri, { manifest: manifest, rootURI: rootURI });

      // If resolution found, store the resolution; otherwise,
      // skip storing it as runtime lookup will handle this
      if (!resolved)
        return agg;
      agg[mod] = resolved;
      return agg;
    }, {});

    let includes = keys(results[uri]);
    let count = 0;
    let subcallback = () => { if (++count >= includes.length) callback(results) };
    includes.map(id => {
      let moduleURI = results[uri][id];
      if (!results[moduleURI])
        findAllModuleIncludes(moduleURI, options, results, subcallback);
      else
        subcallback();
    });
  });
}

// From Substack's detector
// https://github.com/substack/node-detective
//
// Given a resource URI or source, return an array of strings passed into
// the require statements from the source
function findModuleIncludes (uri, callback) {
  let src = isAbsoluteURI(uri) ? readURI(uri) : uri;
  let modules = [];

  walk(src, function (node) {
    if (isRequire(node))
      modules.push(node.arguments[0].value);
  });

  callback(modules);
}

function walk (src, callback) {
  // Import Reflect.jsm from here to prevent loading it until someone uses it
  let { Reflect } = Cu.import("resource://gre/modules/reflect.jsm", {});
  let nodes = Reflect.parse(src);
  traverse(nodes, callback);
}

function traverse (node, cb) {
  if (Array.isArray(node)) {
    node.map(x => {
      if (x != null) {
        x.parent = node;
        traverse(x, cb);
      }
    });
  }
  else if (node && typeof node === 'object') {
    cb(node);
    keys(node).map(key => {
      if (key === 'parent' || !node[key]) return;
      node[key].parent = node;
      traverse(node[key], cb);
    });
  }
}

// From Substack's detector
// https://github.com/substack/node-detective
// Check an AST node to see if its a require statement.
// A modification added to only evaluate to true if it actually
// has a value being passed in as an argument
function isRequire (node) {
  var c = node.callee;
  return c
    && node.type === 'CallExpression'
    && c.type === 'Identifier'
    && c.name === 'require'
    && node.arguments.length
   && node.arguments[0].type === 'Literal';
}

module.exports = iced(Loader);
});
