'use strict';

Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.importFactory = importFactory;

var _is = require("../../utils/is");

var _factory = require("../../utils/factory");

var _object = require("../../utils/object");

var _array = require("../../utils/array");

var _ArgumentsError = require("../../error/ArgumentsError");

var _log = require("../../utils/log");

function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }

function _typeof(obj) { if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); }

function importFactory(typed, load, math, factories) {
  /**
   * Import functions from an object or a module
   *
   * Syntax:
   *
   *    math.import(object)
   *    math.import(object, options)
   *
   * Where:
   *
   * - `object: Object`
   *   An object with functions to be imported.
   * - `options: Object` An object with import options. Available options:
   *   - `override: boolean`
   *     If true, existing functions will be overwritten. False by default.
   *   - `silent: boolean`
   *     If true, the function will not throw errors on duplicates or invalid
   *     types. False by default.
   *   - `wrap: boolean`
   *     If true, the functions will be wrapped in a wrapper function
   *     which converts data types like Matrix to primitive data types like Array.
   *     The wrapper is needed when extending math.js with libraries which do not
   *     support these data type. False by default.
   *
   * Examples:
   *
   *    // define new functions and variables
   *    math.import({
   *      myvalue: 42,
   *      hello: function (name) {
   *        return 'hello, ' + name + '!'
   *      }
   *    })
   *
   *    // use the imported function and variable
   *    math.myvalue * 2               // 84
   *    math.hello('user')             // 'hello, user!'
   *
   *    // import the npm module 'numbers'
   *    // (must be installed first with `npm install numbers`)
   *    math.import(require('numbers'), {wrap: true})
   *
   *    math.fibonacci(7) // returns 13
   *
   * @param {Object | Array} object   Object with functions to be imported.
   * @param {Object} [options]        Import options.
   */
  function mathImport(object, options) {
    var num = arguments.length;

    if (num !== 1 && num !== 2) {
      throw new _ArgumentsError.ArgumentsError('import', num, 1, 2);
    }

    if (!options) {
      options = {};
    } // TODO: allow a typed-function with name too


    if ((0, _factory.isFactory)(object)) {
      _importFactory(object, options);
    } else if ((0, _object.isLegacyFactory)(object)) {
      _importLegacyFactory(object, options);
    } else if (Array.isArray(object)) {
      object.forEach(function (entry) {
        return mathImport(entry, options);
      });
    } else if (_typeof(object) === 'object') {
      // a map with functions
      for (var name in object) {
        if (object.hasOwnProperty(name)) {
          var value = object[name];

          if ((0, _factory.isFactory)(value)) {
            _importFactory(value, options, name);
          } else if (isSupportedType(value)) {
            _import(name, value, options);
          } else if ((0, _object.isLegacyFactory)(object)) {
            _importLegacyFactory(object, options);
          } else {
            mathImport(value, options);
          }
        }
      }
    } else {
      if (!options.silent) {
        throw new TypeError('Factory, Object, or Array expected');
      }
    }
  }
  /**
   * Add a property to the math namespace
   * @param {string} name
   * @param {*} value
   * @param {Object} options  See import for a description of the options
   * @private
   */


  function _import(name, value, options) {
    // TODO: refactor this function, it's to complicated and contains duplicate code
    if (options.wrap && typeof value === 'function') {
      // create a wrapper around the function
      value = _wrap(value);
    } // turn a plain function with a typed-function signature into a typed-function


    if (hasTypedFunctionSignature(value)) {
      value = typed(name, _defineProperty({}, value.signature, value));
    }

    if (isTypedFunction(math[name]) && isTypedFunction(value)) {
      if (options.override) {
        // give the typed function the right name
        value = typed(name, value.signatures);
      } else {
        // merge the existing and typed function
        value = typed(math[name], value);
      }

      math[name] = value;
      delete factories[name];

      _importTransform(name, value);

      math.emit('import', name, function resolver() {
        return value;
      });
      return;
    }

    if (math[name] === undefined || options.override) {
      math[name] = value;
      delete factories[name];

      _importTransform(name, value);

      math.emit('import', name, function resolver() {
        return value;
      });
      return;
    }

    if (!options.silent) {
      throw new Error('Cannot import "' + name + '": already exists');
    }
  }

  function _importTransform(name, value) {
    if (value && typeof value.transform === 'function') {
      math.expression.transform[name] = value.transform;

      if (allowedInExpressions(name)) {
        math.expression.mathWithTransform[name] = value.transform;
      }
    } else {
      // remove existing transform
      delete math.expression.transform[name];

      if (allowedInExpressions(name)) {
        math.expression.mathWithTransform[name] = value;
      }
    }
  }

  function _deleteTransform(name) {
    delete math.expression.transform[name];

    if (allowedInExpressions(name)) {
      math.expression.mathWithTransform[name] = math[name];
    } else {
      delete math.expression.mathWithTransform[name];
    }
  }
  /**
   * Create a wrapper a round an function which converts the arguments
   * to their primitive values (like convert a Matrix to Array)
   * @param {Function} fn
   * @return {Function} Returns the wrapped function
   * @private
   */


  function _wrap(fn) {
    var wrapper = function wrapper() {
      var args = [];

      for (var i = 0, len = arguments.length; i < len; i++) {
        var arg = arguments[i];
        args[i] = arg && arg.valueOf();
      }

      return fn.apply(math, args);
    };

    if (fn.transform) {
      wrapper.transform = fn.transform;
    }

    return wrapper;
  }
  /**
   * Import an instance of a factory into math.js
   * @param {{factory: Function, name: string, path: string, math: boolean}} factory
   * @param {Object} options  See import for a description of the options
   * @private
   */
  // TODO: _importLegacyFactory is deprecated since v6.0.0, clean up some day


  function _importLegacyFactory(factory, options) {
    (0, _log.warnOnce)('Factories of type { name, factory } are deprecated since v6. ' + 'Please create your factory functions using the math.factory function.');

    if (typeof factory.name === 'string') {
      var name = factory.name;
      var existingTransform = name in math.expression.transform;
      var namespace = factory.path ? (0, _object.traverse)(math, factory.path) : math;
      var existing = namespace.hasOwnProperty(name) ? namespace[name] : undefined;

      var resolver = function resolver() {
        var instance = load(factory);

        if (instance && typeof instance.transform === 'function') {
          throw new Error('Transforms cannot be attached to factory functions. ' + 'Please create a separate function for it with exports.path="expression.transform"');
        }

        if (isTypedFunction(existing) && isTypedFunction(instance)) {
          if (options.override) {// replace the existing typed function (nothing to do)
          } else {
            // merge the existing and new typed function
            instance = typed(existing, instance);
          }

          return instance;
        }

        if (existing === undefined || options.override) {
          return instance;
        }

        if (options.silent) {
          return existing;
        } else {
          throw new Error('Cannot import "' + name + '": already exists');
        }
      };

      if (factory.lazy !== false) {
        (0, _object.lazy)(namespace, name, resolver);

        if (existingTransform) {
          _deleteTransform(name);
        } else {
          if (factory.path === 'expression.transform' || legacyFactoryAllowedInExpressions(factory)) {
            (0, _object.lazy)(math.expression.mathWithTransform, name, resolver);
          }
        }
      } else {
        namespace[name] = resolver();

        if (existingTransform) {
          _deleteTransform(name);
        } else {
          if (factory.path === 'expression.transform' || legacyFactoryAllowedInExpressions(factory)) {
            math.expression.mathWithTransform[name] = resolver();
          }
        }
      }

      var key = factory.path ? factory.path + '.' + factory.name : factory.name;
      factories[key] = factory;
      math.emit('import', name, resolver, factory.path);
    } else {
      // unnamed factory.
      // no lazy loading
      load(factory);
    }
  }
  /**
   * Import an instance of a factory into math.js
   * @param {function(scope: object)} factory
   * @param {Object} options  See import for a description of the options
   * @param {string} [name=factory.name] Optional custom name
   * @private
   */


  function _importFactory(factory, options) {
    var name = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : factory.fn;

    if ((0, _array.contains)(name, '.')) {
      throw new Error('Factory name should not contain a nested path. ' + 'Name: ' + JSON.stringify(name));
    }

    var namespace = isTransformFunctionFactory(factory) ? math.expression.transform : math;
    var existingTransform = name in math.expression.transform;
    var existing = namespace.hasOwnProperty(name) ? namespace[name] : undefined;

    var resolver = function resolver() {
      // collect all dependencies, handle finding both functions and classes and other special cases
      var dependencies = {};
      factory.dependencies.map(_factory.stripOptionalNotation).forEach(function (dependency) {
        if ((0, _array.contains)(dependency, '.')) {
          throw new Error('Factory dependency should not contain a nested path. ' + 'Name: ' + JSON.stringify(dependency));
        }

        if (dependency === 'math') {
          dependencies.math = math;
        } else if (dependency === 'mathWithTransform') {
          dependencies.mathWithTransform = math.expression.mathWithTransform;
        } else if (dependency === 'classes') {
          // special case for json reviver
          dependencies.classes = math;
        } else {
          dependencies[dependency] = math[dependency];
        }
      });
      var instance =
      /* #__PURE__ */
      factory(dependencies);

      if (instance && typeof instance.transform === 'function') {
        throw new Error('Transforms cannot be attached to factory functions. ' + 'Please create a separate function for it with exports.path="expression.transform"');
      }

      if (isTypedFunction(existing) && isTypedFunction(instance)) {
        if (options.override) {// replace the existing typed function (nothing to do)
        } else {
          // merge the existing and new typed function
          instance = typed(existing, instance);
        }

        return instance;
      }

      if (existing === undefined || options.override) {
        return instance;
      }

      if (options.silent) {
        return existing;
      } else {
        throw new Error('Cannot import "' + name + '": already exists');
      }
    }; // TODO: add unit test with non-lazy factory


    if (!factory.meta || factory.meta.lazy !== false) {
      (0, _object.lazy)(namespace, name, resolver); // FIXME: remove the `if (existing &&` condition again. Can we make sure subset is loaded before subset.transform? (Name collision, and no dependencies between the two)

      if (existing && existingTransform) {
        _deleteTransform(name);
      } else {
        if (isTransformFunctionFactory(factory) || factoryAllowedInExpressions(factory)) {
          (0, _object.lazy)(math.expression.mathWithTransform, name, function () {
            return namespace[name];
          });
        }
      }
    } else {
      namespace[name] = resolver(); // FIXME: remove the `if (existing &&` condition again. Can we make sure subset is loaded before subset.transform? (Name collision, and no dependencies between the two)

      if (existing && existingTransform) {
        _deleteTransform(name);
      } else {
        if (isTransformFunctionFactory(factory) || factoryAllowedInExpressions(factory)) {
          (0, _object.lazy)(math.expression.mathWithTransform, name, function () {
            return namespace[name];
          });
        }
      }
    } // TODO: improve factories, store a list with imports instead which can be re-played


    factories[name] = factory;
    math.emit('import', name, resolver);
  }
  /**
   * Check whether given object is a type which can be imported
   * @param {Function | number | string | boolean | null | Unit | Complex} object
   * @return {boolean}
   * @private
   */


  function isSupportedType(object) {
    return typeof object === 'function' || typeof object === 'number' || typeof object === 'string' || typeof object === 'boolean' || object === null || (0, _is.isUnit)(object) || (0, _is.isComplex)(object) || (0, _is.isBigNumber)(object) || (0, _is.isFraction)(object) || (0, _is.isMatrix)(object) || Array.isArray(object);
  }
  /**
   * Test whether a given thing is a typed-function
   * @param {*} fn
   * @return {boolean} Returns true when `fn` is a typed-function
   */


  function isTypedFunction(fn) {
    return typeof fn === 'function' && _typeof(fn.signatures) === 'object';
  }

  function hasTypedFunctionSignature(fn) {
    return typeof fn === 'function' && typeof fn.signature === 'string';
  }

  function allowedInExpressions(name) {
    return !unsafe.hasOwnProperty(name);
  }

  function legacyFactoryAllowedInExpressions(factory) {
    return factory.path === undefined && !unsafe.hasOwnProperty(factory.name);
  }

  function factoryAllowedInExpressions(factory) {
    return factory.fn.indexOf('.') === -1 && // FIXME: make checking on path redundant, check on meta data instead
    !unsafe.hasOwnProperty(factory.fn) && (!factory.meta || !factory.meta.isClass);
  }

  function isTransformFunctionFactory(factory) {
    return factory !== undefined && factory.meta !== undefined && factory.meta.isTransformFunction === true || false;
  } // namespaces and functions not available in the parser for safety reasons


  var unsafe = {
    'expression': true,
    'type': true,
    'docs': true,
    'error': true,
    'json': true,
    'chain': true // chain method not supported. Note that there is a unit chain too.

  };
  return mathImport;
}