/**
 * @fileoverview A class for representing built-in EE API Function.
 *
 * Earth Engine can dynamically produce a JSON array listing the
 * algorithms available to the user.  Each item in the dictionary identifies
 * the name and return type of the algorithm, the name and type of its
 * arguments, whether they're required or optional, default values and docs
 * for each argument and the algorithms as a whole.
 *
 * This class manages the algorithm dictionary and creates JavaScript functions
 * to apply each EE algorithm.
 */

goog.provide('ee.ApiFunction');

goog.require('ee.ComputedObject');
goog.require('ee.Function');
goog.require('ee.Types');
goog.require('ee.data');
goog.require('ee.rpc_node');
goog.require('goog.object');
goog.requireType('ee.Encodable');
goog.requireType('ee.api');



/**
 * Recursively deletes the __proto__ property from objects to prevent prototype
 * pollution.
 * @param {*} obj The object to sanitize.
 * @param {!Set<*>=} opt_seen A set of already visited objects to handle
 *     circular references.
 * @return {*} The sanitized object.
 */
function deleteProto(obj, opt_seen) {
  if (!(obj && typeof obj === 'object')) {
    return obj;
  }
  const seen = opt_seen || new Set();
  if (seen.has(obj)) return obj;
  seen.add(obj);
  if (Object.prototype.hasOwnProperty.call(obj, 'constructor')) {
    delete obj['constructor'];
  }
  for (const key in obj) {
    if (key === '__proto__') {
      continue;
    }
    if (Object.prototype.hasOwnProperty.call(obj, key)) {
      deleteProto(obj[key], seen);
    }
  }
  return obj;
}


/**
 * Creates a function defined by the EE API.
 * @param {string} name The name of the function.
 * @param {!ee.Function.Signature|!ee.data.AlgorithmSignature=} opt_signature
 *     The signature of the function. If unspecified, looked up dynamically.
 * @constructor
 * @extends {ee.Function}
 */
ee.ApiFunction = function(name, opt_signature) {
  if (typeof name !== 'string') {
    throw new Error('ApiFunction name must be a string.');
  }
  if (opt_signature === undefined) {
    return ee.ApiFunction.lookup(name);
  } else if (!(this instanceof ee.ApiFunction)) {
    return ee.ComputedObject.construct(ee.ApiFunction, arguments);
  }

  if (opt_signature === null || typeof opt_signature !== 'object') {
    throw new Error('opt_signature must be an object.');
  }
  deleteProto(opt_signature);
  /**
   * The signature of this API function.
   * @const {!ee.Function.Signature}
   * @private
   */
  this.signature_ = /** @type {!ee.Function.Signature} */ (
      goog.object.unsafeClone(opt_signature));
  this.signature_['name'] = name;
};
goog.inherits(ee.ApiFunction, ee.Function);
// Exporting manually to avoid marking the class public in the docs.
goog.exportSymbol('ee.ApiFunction', ee.ApiFunction);


/**
 * Calls a named API function with the given positional arguments.
 *
 * @param {string} name The name of the API function to call.
 * @param {...*} var_args Positional arguments to pass to the function.
 * @return {!ee.ComputedObject} An object representing the called function.
 *     If the signature specifies a recognized return type, the returned
 *     value will be cast to that type.
 * @export
 */
ee.ApiFunction._call = function(name, var_args) {
  if (typeof name !== 'string') {
    throw new Error('ApiFunction name must be a string.');
  }
  const args = Array.prototype.slice.call(arguments, 1);
  for (let i = 0; i < args.length; i++) {
    deleteProto(args[i]);
  }
  return ee.Function.prototype.call.apply(ee.ApiFunction.lookup(name), args);
};


/**
 * Call a named API function with a dictionary of named arguments.
 *
 * @param {string} name The name of the API function to call.
 * @param {!Object} namedArgs A dictionary of arguments to the function.
 * @return {!ee.ComputedObject} An object representing the called function.
 *     If the signature specifies a recognized return type, the returned
 *     value will be cast to that type.
 * @export
 */
ee.ApiFunction._apply = function(name, namedArgs) {
  if (typeof name !== 'string') {
    throw new Error('ApiFunction name must be a string.');
  }
  if (namedArgs === null || typeof namedArgs !== 'object') {
    throw new Error('namedArgs must be an object.');
  }
  deleteProto(namedArgs);
  return ee.ApiFunction.lookup(name).apply(namedArgs);
};





/**
 * @param {function(*): *} encoder A function that can be called to encode
 *     the components of an object.
 * @return {*} The encoded form of the object.
 * @override
 */
ee.ApiFunction.prototype.encode = function(encoder) {
  return this.signature_['name'];
};


/** @override @return {!ee.api.ValueNode} */
ee.ApiFunction.prototype.encodeCloudInvocation = function(
    /** !ee.Encodable.Serializer */ serializer, /** ? */ args) {
  return ee.rpc_node.functionByName(this.signature_['name'], args);
};


/** @override @return {!ee.Function.Signature} */
ee.ApiFunction.prototype.getSignature = function() {
  return this.signature_;
};


/**
 * A dictionary of functions defined by the API server.
 *
 * @type {?Object.<!ee.ApiFunction>}
 * @private
 */
ee.ApiFunction.api_ = null;


/**
 * A set of algorithm names containing all algorithms that have been bound to
 * a function so far using importApi().
 *
 * @type {!Object.<boolean>}
 * @private
 */
ee.ApiFunction.boundSignatures_ = {};


/**
 * @return {!Object.<!ee.Function.Signature>} A map from the name to signature
 *     for all API functions.
 */
ee.ApiFunction.allSignatures = function() {
  ee.ApiFunction.initialize();
  return goog.object.map(ee.ApiFunction.api_, function(func) {
    return func.getSignature();
  });
};


/**
 * Returns the functions that have not been bound using importApi() yet.
 *
 * @return {!Object.<!ee.ApiFunction>} A map from name to function.
 */
ee.ApiFunction.unboundFunctions = function() {
  ee.ApiFunction.initialize();
  return goog.object.filter(ee.ApiFunction.api_, function(func, name) {
    return !ee.ApiFunction.boundSignatures_[name];
  });
};


/**
 * Looks up an API function by name.
 *
 * @param {string} name The name of the function to get.
 * @return {!ee.ApiFunction} The requested function.
 * @export
 */
ee.ApiFunction.lookup = function(name) {
  const func = ee.ApiFunction.lookupInternal(name);
  if (!func) {
    throw Error('Unknown built-in function name: ' + name);
  }
  return func;
};


/**
 * Looks up an API function by name, but doesn't throw an error.
 *
 * @param {string} name The name of the function to get.
 * @return {!ee.ApiFunction|null} The requested function or null if it's not found.
 */
ee.ApiFunction.lookupInternal = function(name) {
  ee.ApiFunction.initialize();
  return ee.ApiFunction.api_.hasOwnProperty(name) ? ee.ApiFunction.api_[name] : null;
};


/**
 * Initializes the list of signatures from the Earth Engine front-end.
 *
 * @param {function()=} opt_successCallback An optional success callback.
 *     If not supplied, the call is made synchronously.
 * @param {function(!Error)=} opt_failureCallback An optional failure callback.
 *     Only valid if opt_successCallback is specified.
 */
ee.ApiFunction.initialize = function(opt_successCallback, opt_failureCallback) {
  if (!ee.ApiFunction.api_) {
    /**
     * @param {?ee.data.AlgorithmsRegistry} data
     * @param {string=} opt_error
     */
    const callback = function(data, opt_error) {
      if (opt_error) {
        if (opt_failureCallback) {
          opt_failureCallback(Error(opt_error));
        }
        return;
      }

      ee.ApiFunction.api_ = goog.object.map(data, function(sig, name) {
        // Strip type parameters.
        sig.returns = sig.returns.replace(/<.*>/, '');
        for (let i = 0; i < sig.args.length; i++) {
          sig.args[i].type = sig.args[i].type.replace(/<.*>/, '');
        }
        return new ee.ApiFunction(name, sig);
      });
      if (opt_successCallback) opt_successCallback();
    };
    if (opt_successCallback) {
      ee.data.getAlgorithms(callback);
    } else {
      callback(
          /** @type {!ee.data.AlgorithmsRegistry} */ (ee.data.getAlgorithms()));
    }
  } else if (opt_successCallback) {
    // The API signatures have previously been initialized by some
    // other means. Immediately execute the callback.
    opt_successCallback();
  }
};


/**
 * Clears the API functions list so it will be reloaded from the server.
 */
ee.ApiFunction.reset = function() {
  ee.ApiFunction.api_ = null;
  ee.ApiFunction.boundSignatures_ = {};
};


/**
 * Adds all API functions that begin with a given prefix to a target class.
 *
 * @param {!Function|!Object} target The class to add to.
 * @param {string} prefix The prefix to search for in the signatures.
 * @param {string} typeName The name of the object's type. Functions whose
 *     first argument matches this type are bound as instance methods, and
 *     those whose first argument doesn't match are bound as static methods.
 * @param {string=} opt_prepend An optional string to prepend to the names
 *     of the added functions.
 */
ee.ApiFunction.importApi = function(target, prefix, typeName, opt_prepend) {
  ee.ApiFunction.initialize();
  const prepend = opt_prepend || '';
  const api = ee.ApiFunction.api_;
  for (const name in api) {
    // "names" in API either look like:
    // - `Array` (base constructor), or
    // - `Array.method` (methods)

    // Ignore properties inherited from the prototype chain.
    if (!Object.prototype.hasOwnProperty.call(api, name)) {
      continue;
    }
    const apiFunc = api[name];
    const parts = name.split('.');  // Should be api group and api method.
    if (parts.length !== 2 || parts[0] !== prefix) {
      continue;
    }

    const apiMethod = parts[1];

    const fname = prepend + apiMethod;
    const signature = apiFunc.getSignature();

    // Mark signatures as used.
    ee.ApiFunction.boundSignatures_[name] = true;

    // Decide whether this is a static or an instance function.
    let isInstance = false;
    if (signature['args'].length) {
      const firstArgType = signature['args'][0]['type'];
      isInstance = firstArgType != 'Object' &&
           ee.Types.isSubtype(firstArgType, typeName);
    }
    // Assume we have a constructor Function if we get an instance method.
    const destination =
        isInstance ? /** @type {!Function} */ (target).prototype : target;

    if (fname in destination && !destination[fname]['signature']) {
      // Don't overwrite client-defined functions.
      continue;
    }

    // Add the actual function
    /**
     * @param {*} var_args
     * @return {!ee.ComputedObject}
     * @this {*}
     **/
    destination[fname] = function(var_args) {
      const args = Array.prototype.slice.call(arguments, 0);
      for (let i = 0; i < args.length; i++) {
        deleteProto(args[i]);
      }
      return apiFunc.callOrApply(isInstance ? this : undefined, args);
    };
    // Add a friendly formatting.
    destination[fname].toString =
        goog.bind(apiFunc.toString, apiFunc, fname, isInstance);
    // Attach the signature object for documentation generators.
    destination[fname]['signature'] = signature;
  }
};


/**
 * Removes all methods added by importApi() from a target class.
 * @param {!Function|!Object} target The class to remove from.
 */
ee.ApiFunction.clearApi = function(target) {
  const clear = function(target) {
    for (const name in target) {
      if (typeof target[name] === 'function' && target[name]['signature']) {
        delete target[name];
      }
    }
  };
  clear(target);
  clear(/** @type {!Function} */ (target).prototype || {});
};
