import { contains } from './array';
import { pickShallow } from './object';
/**
 * Create a factory function, which can be used to inject dependencies.
 *
 * The created functions are memoized, a consecutive call of the factory
 * with the exact same inputs will return the same function instance.
 * The memoized cache is exposed on `factory.cache` and can be cleared
 * if needed.
 *
 * Example:
 *
 *     const name = 'log'
 *     const dependencies = ['config', 'typed', 'divideScalar', 'Complex']
 *
 *     export const createLog = factory(name, dependencies, ({ typed, config, divideScalar, Complex }) => {
 *       // ... create the function log here and return it
 *     }
 *
 * @param {string} name           Name of the function to be created
 * @param {string[]} dependencies The names of all required dependencies
 * @param {function} create       Callback function called with an object with all dependencies
 * @param {Object} [meta]         Optional object with meta information that will be attached
 *                                to the created factory function as property `meta`.
 * @returns {function}
 */

export function factory(name, dependencies, create, meta) {
  function assertAndCreate(scope) {
    // we only pass the requested dependencies to the factory function
    // to prevent functions to rely on dependencies that are not explicitly
    // requested.
    var deps = pickShallow(scope, dependencies.map(stripOptionalNotation));
    assertDependencies(name, dependencies, scope);
    return create(deps);
  }

  assertAndCreate.isFactory = true;
  assertAndCreate.fn = name;
  assertAndCreate.dependencies = dependencies.slice().sort();

  if (meta) {
    assertAndCreate.meta = meta;
  }

  return assertAndCreate;
}
/**
 * Sort all factories such that when loading in order, the dependencies are resolved.
 *
 * @param {Array} factories
 * @returns {Array} Returns a new array with the sorted factories.
 */

export function sortFactories(factories) {
  var factoriesByName = {};
  factories.forEach(function (factory) {
    factoriesByName[factory.fn] = factory;
  });

  function containsDependency(factory, dependency) {
    // TODO: detect circular references
    if (isFactory(factory)) {
      if (contains(factory.dependencies, dependency.fn || dependency.name)) {
        return true;
      }

      if (factory.dependencies.some(function (d) {
        return containsDependency(factoriesByName[d], dependency);
      })) {
        return true;
      }
    }

    return false;
  }

  var sorted = [];

  function addFactory(factory) {
    var index = 0;

    while (index < sorted.length && !containsDependency(sorted[index], factory)) {
      index++;
    }

    sorted.splice(index, 0, factory);
  } // sort regular factory functions


  factories.filter(isFactory).forEach(addFactory); // sort legacy factory functions AFTER the regular factory functions

  factories.filter(function (factory) {
    return !isFactory(factory);
  }).forEach(addFactory);
  return sorted;
} // TODO: comment or cleanup if unused in the end

export function create(factories) {
  var scope = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
  sortFactories(factories).forEach(function (factory) {
    return factory(scope);
  });
  return scope;
}
/**
 * Test whether an object is a factory. This is the case when it has
 * properties name, dependencies, and a function create.
 * @param {*} obj
 * @returns {boolean}
 */

export function isFactory(obj) {
  return typeof obj === 'function' && typeof obj.fn === 'string' && Array.isArray(obj.dependencies);
}
/**
 * Assert that all dependencies of a list with dependencies are available in the provided scope.
 *
 * Will throw an exception when there are dependencies missing.
 *
 * @param {string} name   Name for the function to be created. Used to generate a useful error message
 * @param {string[]} dependencies
 * @param {Object} scope
 */

export function assertDependencies(name, dependencies, scope) {
  var allDefined = dependencies.filter(function (dependency) {
    return !isOptionalDependency(dependency);
  }) // filter optionals
  .every(function (dependency) {
    return scope[dependency] !== undefined;
  });

  if (!allDefined) {
    var missingDependencies = dependencies.filter(function (dependency) {
      return scope[dependency] === undefined;
    }); // TODO: create a custom error class for this, a MathjsError or something like that

    throw new Error("Cannot create function \"".concat(name, "\", ") + "some dependencies are missing: ".concat(missingDependencies.map(function (d) {
      return "\"".concat(d, "\"");
    }).join(', '), "."));
  }
}
export function isOptionalDependency(dependency) {
  return dependency && dependency[0] === '?';
}
export function stripOptionalNotation(dependency) {
  return dependency && dependency[0] === '?' ? dependency.slice(1) : dependency;
}