'use strict';

/**
 * @ngdoc function
 * @name angular.injector
 * @function
 *
 * @description
 * Creates an injector function that can be used for retrieving services as well as for
 * dependency injection (see {@link guide/dev_guide.di dependency injection}).
 *
 * Angular creates an injector automatically for the root scope and it is available as the
 * {@link angular.scope.$service $service} property. Creating an injector doesn't automatically
 * create all of the `$eager` {@link angular.service services}. You have to call `injector.eager()`
 * to initialize them.
 *
 * @param {Object=} [factoryScope={}] The `this` for the service factory function.
 * @param {Object.<string, function()>=} [factories=angular.service] Map of the service factory
 *     functions.
 * @param {Object.<string, function()>=} [instanceCache={}] Place where instances of services are
 *     saved for reuse. Can also be used to override services specified by `serviceFactory`
 *     (useful in tests).
 * @returns {function()} Injector function:
 *
 *   * `injector(serviceName)`:
 *     * `serviceName` - `{string=}` - Name of the service to retrieve.
 *
 * The injector function also has these properties:
 *
 *   * An `invoke` property which can be used to invoke methods with dependency-injected arguments.
 *    `injector.invoke(self, fn, curryArgs)`
 *     * `self` -  The "`this`" to be used when invoking the function.
 *     * `fn` - The function to be invoked. The function may have the `$inject` property that
 *        lists the set of arguments which should be auto-injected.
 *        (see {@link guide/dev_guide.di dependency injection}).
 *     * `curryArgs(array)` - Optional array of arguments to pass to the function
 *        invocation after the injection arguments (also known as curry arguments or currying).
 *   * An `eager` property which is used to initialize the eager services.
 *     `injector.eager()`
 */
function createInjector(factoryScope, factories, instanceCache) {
  factories = factories || angularService;
  instanceCache = instanceCache || {};
  factoryScope = factoryScope || {};
  injector.invoke = invoke;

  injector.eager = function() {
    forEach(factories, function(factory, name){
      if (factory.$eager)
        injector(name);

      if (factory.$creation)
        throw new Error("Failed to register service '" + name +
        "': $creation property is unsupported. Use $eager:true or see release notes.");
    });
  };
  return injector;

  function injector(value){
    if (!(value in instanceCache)) {
      var factory = factories[value];
      if (!factory) throw Error("Unknown provider for '"+value+"'.");
      inferInjectionArgs(factory);
      instanceCache[value] = invoke(factoryScope, factory);
    }
    return instanceCache[value];
  }

  function invoke(self, fn, args){
    args = args || [];
    var injectNames = fn.$inject || [];
    var i = injectNames.length;
    while(i--) {
      args.unshift(injector(injectNames[i]));
    }
    return fn.apply(self, args);
  }
}

/**
 * THIS IS NOT PUBLIC DOC YET!
 *
 * @name angular.annotate
 * @function
 *
 * @description
 * Annotate the function with injection arguments. This is equivalent to setting the `$inject`
 * property as described in {@link guide.di dependency injection}.
 *
 * <pre>
 * var MyController = angular.annotate('$location', function($location){ ... });
 * </pre>
 *
 * is the same as
 *
 * <pre>
 * var MyController = function($location){ ... };
 * MyController.$inject = ['$location'];
 * </pre>
 *
 * @param {String|Array} serviceName... zero or more service names to inject into the
 *     `annotatedFunction`.
 * @param {function} annotatedFunction function to annotate with `$inject`
 *     functions.
 * @returns {function} `annotatedFunction`
 */
function annotate(services, fn) {
  if (services instanceof Array) {
    fn.$inject = services;
    return fn;
  } else {
    var i = 0,
        length = arguments.length - 1, // last one is the destination function
        $inject = arguments[length].$inject = [];
    for (; i < length; i++) {
      $inject.push(arguments[i]);
    }
    return arguments[length]; // return the last one
  }
}

function angularServiceInject(name, fn, inject, eager) {
  angularService(name, fn, {$inject:inject, $eager:eager});
}


/**
 * @returns the $inject property of function. If not found the
 * the $inject is computed by looking at the toString of function and
 * extracting all arguments which and assuming that they are the
 * injection names.
 */
var FN_ARGS = /^function\s*[^\(]*\(([^\)]*)\)/m;
var FN_ARG_SPLIT = /,/;
var FN_ARG = /^\s*(.+?)\s*$/;
var STRIP_COMMENTS = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg;
function inferInjectionArgs(fn) {
  assertArgFn(fn);
  if (!fn.$inject) {
    var args = fn.$inject = [];
    var fnText = fn.toString().replace(STRIP_COMMENTS, '');
    var argDecl = fnText.match(FN_ARGS);
    forEach(argDecl[1].split(FN_ARG_SPLIT), function(arg){
      arg.replace(FN_ARG, function(all, name){
        args.push(name);
      });
    });
  }
  return fn.$inject;
}
