/**
 * @fileoverview Initialization and convenience functions for the EE library.
 */

goog.provide('ee');

goog.require('ee.ApiFunction');
goog.require('ee.Collection');
goog.require('ee.ComputedObject');
goog.require('ee.CustomFunction');
goog.require('ee.Date');
goog.require('ee.Dictionary');
goog.require('ee.Element');
goog.require('ee.Encodable');
goog.require('ee.Feature');
goog.require('ee.FeatureCollection');
goog.require('ee.Filter');
goog.require('ee.Function');
goog.require('ee.Geometry');
goog.require('ee.Image');
goog.require('ee.ImageCollection');
goog.require('ee.List');
goog.require('ee.Number');
goog.require('ee.String');
goog.require('ee.Terrain');
goog.require('ee.Types');
/** @suppress {extraRequire} */
goog.require('ee.batch');
goog.require('ee.data');
goog.require('goog.array');
goog.require('goog.object');


/**
 * Initialize the library.  If this hasn't been called by the time any
 * object constructor is used, it will be called then.  If this is called
 * a second time with a different baseurl or tileurl, this doesn't do an
 * un-initialization of e.g.: the previously loaded Algorithms, but will
 * overwrite them and let point at alternate servers.
 *
 * If initialize() is first called in asynchronous mode (by passing a success
 * callback), any future asynchronous mode calls will add their callbacks to a
 * queue and all the callbacks will be run together.
 *
 * If a synchronous mode call is made after any number of asynchronous calls,
 * it will block and execute all the previously supplied callbacks before
 * returning.
 *
 * In most cases, an authorization token should be set before the library
 * is initialized, either with ee.data.authorize() or ee.data.setAuthToken().
 *
 * In Python, this method is named ee.Initialize, with a capital I.  Note that
 * some parameters differ between JavaScript and Python.  In addition to opt_url
 * and project below, Python also supports:
 *  credentials - a google.oauth2.Credentials object or 'persistent' to use
 *    stored credentials (the default);
 *  http_transport - a httplib2.Http client.
 *
 * @param {string?=} opt_baseurl The Earth Engine REST API endpoint.
 *     (Python argument name: opt_url)
 * @param {string?=} opt_tileurl The Earth Engine REST tile endpoint,
 *     this is optional and defaults to baseurl. (JavaScript only)
 * @param {function()?=} opt_successCallback An optional callback to be invoked
 *     when the initialization is successful. If not provided, the
 *     initialization is done synchronously. (JavaScript only)
 * @param {function(?Error)?=} opt_errorCallback An optional callback to be
 *     invoked with an error if the initialization fails. (JavaScript only)
 * @param {string?=} opt_xsrfToken A string to pass in the "xsrfToken"
 *     parameter of EE API XHRs. (JavaScript only)
 * @param {string?=} opt_project Optional client project ID or number to use
 *     when making API calls. (Python argument name: project)
 * @export
 */
ee.initialize = function(
    opt_baseurl, opt_tileurl, opt_successCallback, opt_errorCallback,
    opt_xsrfToken, opt_project) {
  // If we're already initialized and not getting new parameters, just return.
  if (ee.ready_ == ee.InitState.READY && !opt_baseurl && !opt_tileurl) {
    if (opt_successCallback) {
      opt_successCallback();
    }
    return;
  }

  const isAsynchronous = (opt_successCallback != null);

  // Register the error callback.
  if (opt_errorCallback) {
    if (isAsynchronous) {
      ee.errorCallbacks_.push(opt_errorCallback);
    } else {
      throw Error('Can\'t pass an error callback without a success callback.');
    }
  }

  // If we're already loading, and this is asynchronous, register the success
  // callback and return. Synchronous initialization runs immediately,
  // effectively overriding the currently running asynchronous one.
  if (ee.ready_ == ee.InitState.LOADING && isAsynchronous) {
    ee.successCallbacks_.push(opt_successCallback);
    return;
  }

  ee.ready_ = ee.InitState.LOADING;
  ee.data.initialize(opt_baseurl, opt_tileurl, opt_xsrfToken, opt_project);

  if (isAsynchronous) {
    ee.successCallbacks_.push(opt_successCallback);
    ee.ApiFunction.initialize(
        ee.initializationSuccess_, ee.initializationFailure_);
  } else {
    try {
      ee.ApiFunction.initialize();
      ee.initializationSuccess_();
    } catch (e) {
      ee.initializationFailure_(e);
      throw e;
    }
  }
};


/**
 * Reset the library to its base state. Useful for re-initializing to a
 * different server.
 * @export
 */
ee.reset = function() {
  ee.ready_ = ee.InitState.NOT_READY;
  ee.data.reset();
  ee.ApiFunction.reset();
  ee.Date.reset();
  ee.Dictionary.reset();
  ee.Element.reset();
  ee.Image.reset();
  ee.Feature.reset();
  ee.Collection.reset();
  ee.ImageCollection.reset();
  ee.FeatureCollection.reset();
  ee.Filter.reset();
  ee.Geometry.reset();
  ee.List.reset();
  ee.Number.reset();
  ee.String.reset();
  ee.Terrain.reset();
  ee.resetGeneratedClasses_();
  // Can't simply reassign ee.Algorithms to {} since it's been exported by
  // reference.
  goog.object.clear(ee.Algorithms);
  ee.successCallbacks_ = [];
  ee.errorCallbacks_ = [];
};


/**
 * The possible states for the library initialization function.  We want
 * to prohibit multiple overlapping calls, and allow the user a way to poll
 * to see what the state is.
 *
 * @enum {string}
 * @export
 */
ee.InitState = {
  NOT_READY: 'not_ready',
  LOADING: 'loading',
  READY: 'ready'
};
// @export does not work on enum properties.
goog.exportSymbol('ee.InitState.NOT_READY', ee.InitState.NOT_READY);
goog.exportSymbol('ee.InitState.LOADING', ee.InitState.LOADING);
goog.exportSymbol('ee.InitState.READY', ee.InitState.READY);


/**
 * A flag to indicate the initialization state.
 * @type {!ee.InitState}
 * @private
 */
ee.ready_ = ee.InitState.NOT_READY;


/**
 * The list of callbacks to call on successful initialization. Added by
 * initialize() and cleared by initializationSuccess_() and
 * initializationFailure_().
 * @type {!Array.<function()?>}
 * @private
 */
ee.successCallbacks_ = [];


/**
 * The list of callbacks to call on failed initialization. Added by
 * initialize() and cleared by initializationSuccess_() and
 * initializationFailure_().
 * @type {!Array.<function(!Error)>}
 * @private
 */
ee.errorCallbacks_ = [];


/**
 * @type {number} The size of a tile generated by the /map servlet.
 * @const
 * @export
 */
ee.TILE_SIZE = 256;


/**
 * The list of auto-generated class names.
 * @type {!Array.<string>}
 * @private
 */
ee.generatedClasses_ = [];


/**
 * A dictionary of algorithms that are not bound to a specific class. Can
 * contain nested namespaces (e.g. ee.Algorithms.Landsat.SimpleComposite).
 * @type {!Object.<!Object|!Function>}
 * @export
 */
ee.Algorithms = {};


/**
 * @return {!ee.InitState} The initialization status.
 */
ee.ready = function() {
  return ee.ready_;
};


/**
 * Call a function with the given positional arguments.
 *
 * @param {!ee.Function|string} func The function to call. Either an
 *     ee.Function object or the name of an API function.
 * @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.call = function(func, var_args) {
  if (typeof func === 'string') {
    func = new ee.ApiFunction(func);
  }
  // Extract var_args.
  const args = Array.prototype.slice.call(arguments, 1);
  // Call func.call with the extracted args.
  return ee.Function.prototype.call.apply(func, args);
};


/**
 * Call a function with a dictionary of named arguments.
 *
 * @param {!ee.Function|string} func The function to call. Either an
 *     ee.Function object or the name of an API function.
 * @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.apply = function(func, namedArgs) {
  if (typeof func === 'string') {
    func = new ee.ApiFunction(func);
  }
  return func.apply(namedArgs);
};


/**
 * Finishes the initialization of the library, assuming ApiFunction has been
 * initialized successfully.
 * @private
 */
ee.initializationSuccess_ = function() {
  if (ee.ready_ != ee.InitState.LOADING) {
    // We have already been called. Can happen if a blocking initialization is
    // started while an asynchronous one is in progress. The asynchronous one
    // will report success after the synchronous one has already reported
    // success or failure.
    return;
  }

  try {
    // Update classes with bound methods.
    ee.Date.initialize();
    ee.Dictionary.initialize();
    ee.Element.initialize();
    ee.Image.initialize();
    ee.Feature.initialize();
    ee.Collection.initialize();
    ee.ImageCollection.initialize();
    ee.FeatureCollection.initialize();
    ee.Filter.initialize();
    ee.Geometry.initialize();
    ee.List.initialize();
    ee.Number.initialize();
    ee.String.initialize();
    ee.Terrain.initialize();

    // Generate trivial classes.
    ee.initializeGeneratedClasses_();
    ee.initializeUnboundMethods_();
  } catch (e) {
    ee.initializationFailure_(e);
    return;
  }

  // Declare ourselves ready.
  ee.ready_ = ee.InitState.READY;

  // Clear failure callbacks.
  ee.errorCallbacks_ = [];

  // Call success callbacks.
  while (ee.successCallbacks_.length > 0) {
    // If one of these throws an exception, we explode. Maybe we should ignore
    // it and continue?
    ee.successCallbacks_.shift()();
  }
};


/**
 * Reports initialization failure.
 * @param {!Error} e The cause of the failure.
 * @private
 */
ee.initializationFailure_ = function(e) {
  if (ee.ready_ != ee.InitState.LOADING) {
    // Duplicate call. See reasoning in ee.initializationSuccess_.
    return;
  }

  // Declare ourselves unready.
  ee.ready_ = ee.InitState.NOT_READY;

  // Clear success callbacks.
  ee.successCallbacks_ = [];

  // Call failure callbacks.
  while (ee.errorCallbacks_.length > 0) {
    // If one of these throws an exception, we explode. Maybe we should ignore
    // it and continue?
    ee.errorCallbacks_.shift()(e);
  }
};


/**
 * Wrap an argument in an object of the specified class. This is used to
 * e.g.: promote numbers or strings to Images and arrays to Collections.
 *
 * @param {?} arg The object to promote.
 * @param {string} klass The expected type.
 * @return {?} The argument promoted if the class is recognized, otherwise the
 *     original argument.
 * @private
 * @suppress {accessControls} We are calling functions with partial promotion.
 */
ee.promote_ = function(arg, klass) {
  if (arg === null) {
    return null;
  } else if (arg === undefined) {
    return undefined;
  }

  if (typeof klass != 'string') {  // b/451628586
    throw Error('Unexpected type: ' + Object.prototype.toString.call(klass));
  }

  const exportedEE = goog.global['ee'];

  switch (klass) {
    case 'Image':
      return new ee.Image(/** @type {!Object} */ (arg));
    case 'Feature':
      if (arg instanceof ee.Collection) {
        // This can be quite dangerous on large collections.
        return ee.ApiFunction._call(
            'Feature', ee.ApiFunction._call('Collection.geometry', arg));
      } else {
        return new ee.Feature(/** @type {!Object} */ (arg));
      }
    case 'Element':
      if (arg instanceof ee.Element) {
        // Already an Element.
        return arg;
      } else if (arg instanceof ee.Geometry) {
        // Geometries get promoted to Features.
        return new ee.Feature(/** @type {!ee.Geometry} */ (arg));
      } else if (arg instanceof ee.ComputedObject) {
        // Try a cast.
        const co = /** @type {!ee.ComputedObject} */ (arg);
        return new ee.Element(co.func, co.args, co.varName);
      } else {
        // No way to convert.
        throw Error('Cannot convert ' + arg + ' to Element.');
      }
    case 'Geometry':
      if (arg instanceof ee.FeatureCollection) {
        return ee.ApiFunction._call('Collection.geometry', arg);
      } else {
        return new ee.Geometry(/** @type {?} */ (arg));
      }
    case 'FeatureCollection':
    case 'Collection':  // For now this is synonymous with FeatureCollection.
      if (arg instanceof ee.Collection) {
        return arg;
      } else {
        return new ee.FeatureCollection(/** @type {?} */ (arg));
      }
    case 'ImageCollection':
      return new ee.ImageCollection(/** @type {?} */ (arg));
    case 'Filter':
      return new ee.Filter(/** @type {!Object} */ (arg));
    case 'Algorithm':
      if (typeof arg === 'string') {
        // An API function name.
        return new ee.ApiFunction(arg);
      } else if (typeof arg === 'function') {
        // A native function that needs to be wrapped.
        return ee.CustomFunction.create(
            arg, 'Object', goog.array.repeat('Object', arg.length));
      } else if (arg instanceof ee.Encodable) {
        // An ee.Function or a computed function like the return value of
        // Image.parseExpression().
        return arg;
      } else {
        throw Error('Argument is not a function: ' + arg);
      }
    case 'String':
      if (ee.Types.isString(arg) ||
          arg instanceof ee.String ||
          arg instanceof ee.ComputedObject) {
        return new ee.String(arg);
      } else {
        return arg;
      }
    case 'Dictionary':
      if (ee.Types.isRegularObject(arg)) {
        return arg;
      } else {
        return new ee.Dictionary(/** @type {?} */ (arg));
      }
    case 'List':
      return new ee.List(/** @type {?} */ (arg));
    case 'Number':
    case 'Float':
    case 'Long':
    case 'Integer':
    case 'Short':
    case 'Byte':
      return new ee.Number(/** @type {?} */ (arg));
    default:
      // Handle dynamically generated classes.
      if (klass in exportedEE) {
        if (!(exportedEE[klass].prototype instanceof ee.ComputedObject)) {
          // Block things that should not be replaced.
          throw new Error(
              'Algorithm not an instance of ee.ComputedObject: ' + klass +
              ': ' + arg);
        }

        const ctor = ee.ApiFunction.lookupInternal(klass);
        if (arg instanceof exportedEE[klass]) {
          // Return unchanged.
          return arg;
        } else if (ctor) {
          // The client-side constructor will call the server-side constructor.
          return new exportedEE[klass](arg);
        } else if (typeof arg === 'string') {
          if (arg in exportedEE[klass]) {
            // arg is the name of a method on klass.
            return exportedEE[klass][arg].call();
          } else {
            throw new Error('Unknown algorithm: ' + klass + '.' + arg);
          }
        } else {
          // Client-side cast.
          return new exportedEE[klass](arg);
        }
      } else {
        // Don't know.
        return arg;
      }
  }
};


/**
 * Puts any unbound API methods on ee.Algorithms.
 *
 * @private
 */
ee.initializeUnboundMethods_ = function() {
  const unbound = ee.ApiFunction.unboundFunctions();
  goog.object.getKeys(unbound).sort().forEach(function(name) {
    const func = unbound[name];
    const signature = func.getSignature();
    if (signature['hidden']) {
      return;
    }

    // Create nested objects as needed.
    let nameParts = name.split('.');
    let target = ee.Algorithms;
    target['signature'] = {};
    while (nameParts.length > 1) {
      const first = nameParts[0];
      if (!(first in target)) {
        // We must add a signature property so the Code Editor docbox recognizes
        // these objects as parts of the API.
        target[first] = {'signature': {}};
      }
      target = target[first];
      nameParts = goog.array.slice(nameParts, 1);
    }

    // Attach the function.
    const bound = function(var_args) {
      return func.callOrApply(
          undefined, Array.prototype.slice.call(arguments, 0));
    };
    bound['signature'] = signature;
    bound.toString = goog.bind(func.toString, func);
    target[nameParts[0]] = bound;
  });
};


/**
 * Autogenerate any classes that meet the following criteria:
 *   - There's 1 or more functions named TYPE.*
 *   - There's 1 or more functions that return that type.
 *   - The class doesn't already exist as an ee.TYPE.
 *
 * @private
 * @suppress {accessControls} We update ApiFunction.boundSignatures_.
 */
ee.initializeGeneratedClasses_ = function() {
  const signatures = ee.ApiFunction.allSignatures();

  // Collect all the type names from functions, and all the return types.
  const names = {};
  const returnTypes = {};
  for (const sig in signatures) {
    let type;
    if (sig.indexOf('.') != -1) {
      type = sig.slice(0, sig.indexOf('.'));
    } else {
      type = sig;
    }
    names[type] = true;
    // Strip off extra type info.  e.g.: Dictionary<Object>
    const rtype = signatures[sig]['returns'].replace(/<.*>/, '');
    returnTypes[rtype] = true;
  }

  // Create classes with names in both, excluding any types that already exist.
  const exportedEE = goog.global['ee'];
  for (const name in names) {
    if (name in returnTypes && !(name in exportedEE)) {
      exportedEE[name] = ee.makeClass_(name);
      ee.generatedClasses_.push(name);
      // Mark this class as a constructor if it is.
      if (signatures[name]) {
        exportedEE[name]['signature'] = signatures[name];
        exportedEE[name]['signature']['isConstructor'] = true;
        ee.ApiFunction.boundSignatures_[name] = true;
      } else {
        exportedEE[name]['signature'] = {};
      }
    }
  }
  ee.Types.registerClasses(exportedEE);
};


/**
 * Remove the classes added by initializeGeneratedClasses.
 * @private
 */
ee.resetGeneratedClasses_ = function() {
  const exportedEE = goog.global['ee'];
  for (let i = 0; i < ee.generatedClasses_.length; i++) {
    const name = ee.generatedClasses_[i];
    ee.ApiFunction.clearApi(exportedEE[name]);
    delete exportedEE[name];
  }
  ee.generatedClasses_ = [];
  ee.Types.registerClasses(exportedEE);
};


/**
 * Dynamically make an ee helper class.
 *
 * @param {string} name The name of the class to create.
 * @return {!Function} The generated class.
 * @private
 * @suppress {accessControls}
 */
ee.makeClass_ = function(name) {
  /**
   * Construct a new instance of the given class.
   *
   * @param {...*} var_args The constructor args.  Can be one of:
   *   1) A computed value to be promoted to this type.
   *   2) Arguments to be passed to the algorithm with the same name as
   *      this class.
   *
   * @return {*} The newly created class.
   *
   * @constructor
   * @extends {ee.ComputedObject}
   *
   * TODO(user): Generate docs for these classes.
   */
  const target = function(var_args) {
    const klass = goog.global['ee'][name];
    const args = Array.prototype.slice.call(arguments);
    const onlyOneArg = (args.length == 1);

    // Are we trying to cast something that's already of the right class?
    if (onlyOneArg && args[0] instanceof klass) {
      return args[0];
    }

    // Constructor safety.
    if (!(this instanceof klass)) {
      return ee.ComputedObject.construct(klass, args);
    }

    // Decide whether to call a server-side constructor or just do a
    // client-side cast.
    const ctor = ee.ApiFunction.lookupInternal(name);
    const firstArgIsPrimitive = !(args[0] instanceof ee.ComputedObject);
    let shouldUseConstructor = false;
    if (ctor) {
      if (!onlyOneArg) {
        // Can't client-cast multiple arguments.
        shouldUseConstructor = true;
      } else if (firstArgIsPrimitive) {
        // Can't cast a primitive.
        shouldUseConstructor = true;
      } else if (!args[0].func ||
          args[0].func.getSignature().returns != ctor.getSignature().returns) {
        // We have a single ComputedObject argument. If it returns a different
        // type, we need to call the constructor. Otherwise this is a copy
        // constructor usage, and we can simply cast.
        shouldUseConstructor = true;
      }
    }

    // Apply our decision.
    if (shouldUseConstructor) {
      const namedArgs = ee.Types.useKeywordArgs(args, ctor.getSignature())
          ? args[0] : ctor.nameArgs(args);
      // Call manually to avoid having promote() called on the output.
      target.base(this, 'constructor', ctor, ctor.promoteArgs(namedArgs));
    } else {
      // Just cast and hope for the best.
      if (!onlyOneArg) {
        // We don't know what to do with multiple args.
        throw Error('Too many arguments for ee.' + name + '(): ' + args);
      } else if (firstArgIsPrimitive) {
        // Can't cast a primitive.
        throw Error('Invalid argument for ee.' + name + '(): ' + args +
                    '. Must be a ComputedObject.');
      }
      const theOneArg = args[0];
      target.base(this, 'constructor',
          theOneArg.func, theOneArg.args, theOneArg.varName);
    }
  };
  goog.inherits(target, ee.ComputedObject);
  /**
   * @return {string}
   * @override
   */
  target.prototype.name = function() { return name; };
  ee.ApiFunction.importApi(target, name, name);
  return target;
};

// Set up type promotion rules as soon the library is loaded.
ee.Function.registerPromoter(ee.promote_);
