import _ from 'lodash';
import { uiModules } from 'ui/modules';
/**
 * # `Private()`
 * Private module loader, used to merge angular and require js dependency styles
 * by allowing a require.js module to export a single provider function that will
 * create a value used within an angular application. This provider can declare
 * angular dependencies by listing them as arguments, and can be require additional
 * Private modules.
 *
 * ## Define a private module provider:
 * ```js
 * define(function (require) {
 *   return function PingProvider($http) {
 *     this.ping = function () {
 *       return $http.head('/health-check');
 *     };
 *   };
 * });
 * ```
 *
 * ## Require a private module:
 * ```js
 * define(function (require) {
 *   return function ServerHealthProvider(Private, Promise) {
 *     let ping = Private(require('ui/ping'));
 *     return {
 *       check: Promise.method(function () {
 *         let attempts = 0;
 *         return (function attempt() {
 *           attempts += 1;
 *           return ping.ping()
 *           .catch(function (err) {
 *             if (attempts < 3) return attempt();
 *           })
 *         }())
 *         .then(function () {
 *           return true;
 *         })
 *         .catch(function () {
 *           return false;
 *         });
 *       })
 *     }
 *   };
 * });
 * ```
 *
 * # `Private.stub(provider, newInstance)`
 * `Private.stub()` replaces the instance of a module with another value. This is all we have needed until now.
 *
 * ```js
 * beforeEach(inject(function ($injector, Private) {
 *   Private.stub(
 *     // since this module just exports a function, we need to change
 *     // what Private returns in order to modify it's behavior
 *     require('ui/agg_response/hierarchical/_build_split'),
 *     sinon.stub().returns(fakeSplit)
 *   );
 * }));
 * ```
 *
 * # `Private.swap(oldProvider, newProvider)`
 * This new method does an 1-for-1 swap of module providers, unlike `stub()` which replaces a modules instance.
 * Pass the module you want to swap out, and the one it should be replaced with, then profit.
 *
 * Note: even though this example shows `swap()` being called in a config
 * function, it can be called from anywhere. It is particularly useful
 * in this scenario though.
 *
 * ```js
 * beforeEach(module('kibana', function (PrivateProvider) {
 *   PrivateProvider.swap(
 *     // since the courier is required automatically before the tests are loaded,
 *     // we can't stub it's internal components unless we do so before the
 *     // application starts. This is why angular has config functions
 *     require('ui/courier/_redirect_when_missing'),
 *     function StubbedRedirectProvider($decorate) {
 *       // $decorate is a function that will instantiate the original module when called
 *       return sinon.spy($decorate());
 *     }
 *   );
 * }));
 * ```
 *
 * @param {[type]} prov [description]
 */


const nextId = _.partial(_.uniqueId, 'privateProvider#');

function name(fn) {
  return fn.name || fn.toString().split('\n').shift();
}

uiModules.get('kibana/private')
.provider('Private', function () {
  const provider = this;

  // one cache/swaps per Provider
  const cache = {};
  const swaps = {};

  // return the uniq id for this function
  function identify(fn) {
    if (typeof fn !== 'function') {
      throw new TypeError('Expected private module "' + fn + '" to be a function');
    }

    if (fn.$$id) return fn.$$id;
    else return (fn.$$id = nextId());
  }

  provider.stub = function (fn, instance) {
    cache[identify(fn)] = instance;
    return instance;
  };

  provider.swap = function (fn, prov) {
    const id = identify(fn);
    swaps[id] = prov;
  };

  provider.$get = ['$injector', function PrivateFactory($injector) {

    // prevent circular deps by tracking where we came from
    const privPath = [];
    const pathToString = function () {
      return privPath.map(name).join(' -> ');
    };

    // call a private provider and return the instance it creates
    function instantiate(prov, locals) {
      if (~privPath.indexOf(prov)) {
        throw new Error(
          'Circular reference to "' + name(prov) + '"' +
          ' found while resolving private deps: ' + pathToString()
        );
      }

      privPath.push(prov);

      const context = {};
      let instance = $injector.invoke(prov, context, locals);
      if (!_.isObject(instance)) instance = context;

      privPath.pop();
      return instance;
    }

    // retrieve an instance from cache or create and store on
    function get(id, prov, $delegateId, $delegateProv) {
      if (cache[id]) return cache[id];

      let instance;

      if ($delegateId != null && $delegateProv != null) {
        instance = instantiate(prov, {
          $decorate: _.partial(get, $delegateId, $delegateProv)
        });
      } else {
        instance = instantiate(prov);
      }

      return (cache[id] = instance);
    }

    // main api, get the appropriate instance for a provider
    function Private(prov) {
      let id = identify(prov);
      let $delegateId;
      let $delegateProv;

      if (swaps[id]) {
        $delegateId = id;
        $delegateProv = prov;

        prov = swaps[$delegateId];
        id = identify(prov);
      }

      return get(id, prov, $delegateId, $delegateProv);
    }

    Private.stub = provider.stub;
    Private.swap = provider.swap;

    return Private;
  }];
});
