text: /*global process, Buffer */
  /**
   * @file Manages Salesforce Metadata API
   * @author Shinichi Tomita <shinichi.tomita@gmail.com>
   */
  
  'use strict';
  
  var inherits = require('inherits'),
      events  = require('events'),
      stream  = require('readable-stream'),
      _       = require('lodash/core'),
      jsforce = require('../core'),
      Promise = require('../promise'),
      SOAP    = require('../soap');
  
  /*--------------------------------------------*/
  /**
   * Class for Salesforce Metadata API
   *
   * @class
   * @param {Connection} conn - Connection object
   */
  var Metadata = module.exports = function(conn) {
    this._conn = conn;
  };
  
  
  /**
   * Polling interval in milliseconds
   * @type {Number}
   */
  Metadata.prototype.pollInterval = 1000;
  
  /**
   * Polling timeout in milliseconds
   * @type {Number}
   */
  Metadata.prototype.pollTimeout = 10000;
  
  
  /**
   * Call Metadata API SOAP endpoint
   *
   * @private
   */
  Metadata.prototype._invoke = function(method, message, callback) {
    var soapEndpoint = new SOAP(this._conn, {
      xmlns: "http://soap.sforce.com/2006/04/metadata",
      endpointUrl: this._conn.instanceUrl + "/services/Soap/m/" + this._conn.version
    });
    return soapEndpoint.invoke(method, message).then(function(res) {
      return res.result;
    }).thenCall(callback);
  };
  
  
  /**
   * @typedef {Object} Metadata~MetadataInfo
   * @prop {String} fullName - The name of the component
   */
  
  /**
   * Asynchronously adds one or more new metadata components to the organization.
   *
   * @param {String} type - The type of metadata to create
   * @param {Metadata~MetadataInfo|Array.<Metadata~MetadataInfo>} metadata - Metadata to create
   * @param {Callback.<Metadata~AsyncResult|Array.<Metadata~AsyncResult>>} [callback] - Callback function
   * @returns {Metadata~AsyncResultLocator}
   */
  Metadata.prototype.createAsync = function(type, metadata, callback) {
    if (Number(this._conn.version) > 30) {
      throw new Error("Async metadata CRUD calls are not supported on ver 31.0 or later.");
    }
    var convert = function(md) {
      md["@xsi:type"] = type;
      return md;
    };
    var isArray = _.isArray(metadata);
    metadata = isArray ? _.map(metadata, convert) : convert(metadata);
    var res = this._invoke("create", { metadata: metadata });
    return new AsyncResultLocator(this, res, isArray).thenCall(callback);
  };
  
  /**
   * @typedef {Object} Metadata~SaveResult
   * @prop {Boolean} success - True if metadata is successfully saved
   * @prop {String} fullName - Full name of metadata object
   */
  
  /**
   * @private
   */
  function convertToSaveResult(result) {
    var saveResult = _.clone(result);
    saveResult.success = saveResult.success === 'true';
    return saveResult;
  }
  
  /**
   * @typedef {Object} Metadata~UpsertResult
   * @prop {Boolean} success - True if metadata is successfully saved
   * @prop {String} fullName - Full name of metadata object
   * @prop {Boolean} created - True if metadata is newly created
   */
  
  /**
   * @private
   */
  function convertToUpsertResult(result) {
    var upsertResult = convertToSaveResult(result);
    upsertResult.created = upsertResult.created === 'true';
    return upsertResult;
  }
  
  /**
   * Synonym of Metadata#create().
   *
   * @method Metadata#createSync
   * @param {String} type - The type of metadata to create
   * @param {Metadata~MetadataInfo|Array.<Metadata~MetadataInfo>} metadata - Metadata to create
   * @param {Callback.<Metadata~SaveResult|Array.<Metadata~SaveResult>>} [callback] - Callback function
   * @returns {Promise.<Metadata~SaveResult|Array.<Metadata~SaveResult>>}
   */
  /**
   * Synchronously adds one or more new metadata components to the organization.
   *
   * @method Metadata#create
   * @param {String} type - The type of metadata to create
   * @param {Metadata~MetadataInfo|Array.<Metadata~MetadataInfo>} metadata - Metadata to create
   * @param {Callback.<Metadata~SaveResult|Array.<Metadata~SaveResult>>} [callback] - Callback function
   * @returns {Promise.<Metadata~SaveResult|Array.<Metadata~SaveResult>>}
   */
  Metadata.prototype.createSync =
  Metadata.prototype.create = function(type, metadata, callback) {
    var convert = function(md) {
      md["@xsi:type"] = type;
      return md;
    };
    var isArray = _.isArray(metadata);
    metadata = isArray ? _.map(metadata, convert) : convert(metadata);
    return this._invoke("createMetadata", { metadata: metadata }).then(function(results) {
      return _.isArray(results) ? _.map(results, convertToSaveResult) : convertToSaveResult(results);
    }).thenCall(callback);
  };
  
  /**
   * @private
   */
  function convertToMetadataInfo(rec) {
    var metadataInfo = _.clone(rec);
    delete metadataInfo.$;
    return metadataInfo;
  }
  
  /**
   * Synonym of Metadata#read()
   *
   * @method Metadata#readSync
   * @param {String} type - The type of metadata to read
   * @param {String|Array.<String>} fullNames - full name(s) of metadata objects to read
   * @param {Callback.<Metadata~MetadataInfo|Array.<Metadata~MetadataInfo>>} [callback] - Callback function
   * @returns {Promise.<Array.<Metadata~MetadataInfo|Array.<Metadata~MetadataInfo>>>}
   */
  /**
   * Synchronously read specified metadata components in the organization.
   *
   * @method Metadata#read
   * @param {String} type - The type of metadata to read
   * @param {String|Array.<String>} fullNames - full name(s) of metadata objects to read
   * @param {Callback.<Metadata~MetadataInfo|Array.<Metadata~MetadataInfo>>} [callback] - Callback function
   * @returns {Promise.<Array.<Metadata~MetadataInfo|Array.<Metadata~MetadataInfo>>>}
   */
  Metadata.prototype.readSync =
  Metadata.prototype.read = function(type, fullNames, callback) {
    return this._invoke("readMetadata", { type: type, fullNames: fullNames }).then(function(res) {
      return _.isArray(res.records) ? _.map(res.records, convertToMetadataInfo) : convertToMetadataInfo(res.records);
    }).thenCall(callback);
  };
  
  /**
   * @typedef {Object} Metadata~UpdateMetadataInfo
   * @prop {String} currentName - The API name of the component or field before the update
   * @prop {Metadata~MetadataInfo} metadata - Full specification of the component or field you wish to update
   */
  
  /**
   * Asynchronously updates one or more metadata components in the organization.
   *
   * @param {String} type - The type of metadata to update
   * @param {Metadata~UpdateMetadataInfo|Array.<Metadata~UpdateMetadataInfo>} updateMetadata - Updating metadata
   * @param {Callback.<Metadata~AsyncResult|Array.<Metadata~AsyncResult>>} [callback] - Callback function
   * @returns {Metadata~AsyncResultLocator}
   */
  Metadata.prototype.updateAsync = function(type, updateMetadata, callback) {
    if (Number(this._conn.version) > 30) {
      throw new Error("Async metadata CRUD calls are not supported on ver 31.0 or later.");
    }
    var convert = function(umd) {
      umd.metadata["@xsi:type"] = type;
      return umd;
    };
    var isArray = _.isArray(updateMetadata);
    updateMetadata = isArray ? _.map(updateMetadata, convert) : convert(updateMetadata);
    var res = this._invoke("update", { updateMetadata: updateMetadata });
    return new AsyncResultLocator(this, res, isArray).thenCall(callback);
  };
  
  /**
   * Synonym of Metadata#update().
   *
   * @method Metadata#updateSync
   * @param {String} type - The type of metadata to update
   * @param {Metadata~MetadataInfo|Array.<Metadata~MetadataInfo>} updateMetadata - Updating metadata
   * @param {Callback.<Metadata~SaveResult|Array.<Metadata~SaveResult>>} [callback] - Callback function
   * @returns {Promise.<Metadata~SaveResult|Array.<Metadata~SaveResult>>}
   */
  /**
   * Synchronously updates one or more metadata components in the organization.
   *
   * @method Metadata#update
   * @param {String} type - The type of metadata to update
   * @param {Metadata~MetadataInfo|Array.<Metadata~MetadataInfo>} updateMetadata - Updating metadata
   * @param {Callback.<Metadata~SaveResult|Array.<Metadata~SaveResult>>} [callback] - Callback function
   * @returns {Promise.<Metadata~SaveResult|Array.<Metadata~SaveResult>>}
   */
  Metadata.prototype.updateSync =
  Metadata.prototype.update = function(type, metadata, callback) {
    var convert = function(md) {
      md["@xsi:type"] = type;
      return md;
    };
    var isArray = _.isArray(metadata);
    metadata = isArray ? _.map(metadata, convert) : convert(metadata);
    return this._invoke("updateMetadata", { metadata: metadata }).then(function(results) {
      return _.isArray(results) ? _.map(results, convertToSaveResult) : convertToSaveResult(results);
    }).thenCall(callback);
  };
  
  /**
   * Synonym of Metadata#upsert().
   *
   * @method Metadata#upsertSync
   * @param {String} type - The type of metadata to upsert
   * @param {Metadata~MetadataInfo|Array.<Metadata~MetadataInfo>} metadata - Upserting metadata
   * @param {Callback.<Metadata~UpsertResult|Array.<Metadata~UpsertResult>>} [callback] - Callback function
   * @returns {Promise.<Metadata~UpsertResult|Array.<Metadata~UpsertResult>>}
   */
  /**
   * Upserts one or more components in your organization's data.
   *
   * @method Metadata#upsert
   * @param {String} type - The type of metadata to upsert
   * @param {Metadata~MetadataInfo|Array.<Metadata~MetadataInfo>} metadata - Upserting metadata
   * @param {Callback.<Metadata~UpsertResult|Array.<Metadata~UpsertResult>>} [callback] - Callback function
   * @returns {Promise.<Metadata~UpsertResult|Array.<Metadata~UpsertResult>>}
   */
  Metadata.prototype.upsertSync =
  Metadata.prototype.upsert = function(type, metadata, callback) {
    var convert = function(md) {
      md["@xsi:type"] = type;
      return md;
    };
    var isArray = _.isArray(metadata);
    metadata = isArray ? _.map(metadata, convert) : convert(metadata);
    return this._invoke("upsertMetadata", { metadata: metadata }).then(function(results) {
      return _.isArray(results) ? _.map(results, convertToUpsertResult) : convertToUpsertResult(results);
    }).thenCall(callback);
  };
  
  /**
   * Asynchronously deletes specified metadata components in the organization.
   *
   * @param {String} type - The type of metadata to delete
   * @param {String|Metadata~MetadataInfo|Array.<String>|Array.<Metadata~MetadataInfo>} metadata - The fullName of metadata or metadata info to delete. If it is passed in fullName, the type parameter should not be empty.
   * @param {Callback.<Metadata~AsyncResult|Array.<Metadata~AsyncResult>>} [callback] - Callback function
   * @returns {Metadata~AsyncResultLocator}
   */
  Metadata.prototype.deleteAsync = function(type, metadata, callback) {
    if (Number(this._conn.version) > 30) {
      throw new Error("Async metadata CRUD calls are not supported on ver 31.0 or later.");
    }
    var convert = function(md) {
      if (_.isString(md)) {
        md = { fullName : md };
      }
      md["@xsi:type"] = type;
      return md;
    };
    var isArray = _.isArray(metadata);
    metadata = isArray ? _.map(metadata, convert) : convert(metadata);
    var res = this._invoke("delete", { metadata: metadata });
    return new AsyncResultLocator(this, res, isArray).thenCall(callback);
  };
  
  /**
   * Synonym of Metadata#delete().
   *
   * @deprecated
   * @method Metadata#del
   * @param {String} [type] - The type of metadata to delete
   * @param {String|Metadata~MetadataInfo|Array.<String>|Array.<Metadata~MetadataInfo>} metadata - The fullName of metadata or metadata info to delete. If it is passed in fullName, the type parameter should not be empty.
   * @param {Callback.<Metadata~AsyncResult|Array.<Metadata~AsyncResult>>} [callback] - Callback function
   * @returns {Metadata~AsyncResultLocator}
   */
  /**
   * Synonym of Metadata#delete().
   *
   * @method Metadata#deleteSync
   * @param {String} type - The type of metadata to delete
   * @param {String|Array.<String>} fullNames - The fullName of metadata to delete.
   * @param {Callback.<Metadata~SaveResult|Array.<Metadata~SaveResult>>} [callback] - Callback function
   * @returns {Promise.<Metadata~SaveResult|Array.<Metadata~SaveResult>>}
   */
  
  /**
   * Synchronously deletes specified metadata components in the organization.
   *
   * @method Metadata#delete
   * @param {String} type - The type of metadata to delete
   * @param {String|Array.<String>} fullNames - The fullName of metadata to delete.
   * @param {Callback.<Metadata~SaveResult|Array.<Metadata~SaveResult>>} [callback] - Callback function
   * @returns {Promise.<Metadata~SaveResult|Array.<Metadata~SaveResult>>}
   */
  Metadata.prototype.del =
  Metadata.prototype.deleteSync =
  Metadata.prototype["delete"] = function(type, fullNames, callback) {
    return this._invoke("deleteMetadata", { type: type, fullNames: fullNames }).then(function(results) {
      return _.isArray(results) ? _.map(results, convertToSaveResult) : convertToSaveResult(results);
    }).thenCall(callback);
  };
  
  /**
   * Rename fullname of a metadata component in the organization
   *
   * @param {String} type - The type of metadata to delete
   * @param {String} oldFullName - The original fullName of metadata
   * @param {String} newFullName - The new fullName of metadata
   * @param {Callback.<Metadata~SaveResult>} [callback] - Callback function
   * @returns {Promise.<Metadata~SaveResult>}
   */
  Metadata.prototype.rename = function(type, oldFullName, newFullName, callback) {
    return this._invoke("renameMetadata", { type: type, oldFullName: oldFullName, newFullName: newFullName }).then(function(result) {
      return convertToSaveResult(result);
    }).thenCall(callback);
  };
  
  /**
   * Checks the status of asynchronous metadata calls
   *
   * @param {String|Array.<String>} ids - The asynchronous process ID(s)
   * @param {Callback.<Metadata~AsyncResult|Array.<Metadata~AsyncResult>>} [callback] - Callback function
   * @returns {Metadata~AsyncResultLocator}
   */
  Metadata.prototype.checkStatus = function(ids, callback) {
    var isArray = _.isArray(ids);
    var res = this._invoke("checkStatus", { asyncProcessId: ids });
    return new AsyncResultLocator(this, res, isArray).thenCall(callback);
  };
  
  /**
   * @typedef {Object} Metadata~DescribeMetadataResult
   * @prop {Array.<Object>} metadataObjects - One or more metadata components and their attributes
   * @prop {Array.<String>} metadataObjects.childXmlNames - List of child sub-components for this component
   * @prop {String} metadataObjects.directoryName - The name of the directory in the .zip file that contains this component
   * @prop {Boolean} metadataObjects.inFolder - Indicates whether the component is in a folder or not
   * @prop {Boolean} metadataObjects.metaFile - Indicates whether the component requires an accompanying metadata file
   * @prop {String} metadataObjects.suffix - The file suffix for this component
   * @prop {String} metadataObjects.xmlName - The name of the root element in the metadata file for this component
   * @prop {String} organizationNamespace - The namespace of the organization
   * @prop {Boolean} partialSaveAllowed - Indicates whether rollbackOnError is allowed or not
   * @prop {Boolean} testRequired - Indicates whether tests are required or not
   */
  
  /**
   * Retrieves the metadata which describes your organization, including Apex classes and triggers,
   * custom objects, custom fields on standard objects, tab sets that define an app,
   * and many other components.
   *
   * @param {String} [version] - The API version for which you want metadata; for example, 29.0
   * @param {Callback.<Metadata~DescribeMetadataResult>} [callback] - Callback function
   * @returns {Promise.<Metadata~DescribeMetadataResult>}
   */
  Metadata.prototype.describe = function(version, callback) {
    if (!_.isString(version)) {
      callback = version;
      version = this._conn.version;
    }
    return this._invoke("describeMetadata", { asOfVersion: version }).then(function(res) {
      res.metadataObjects = _.isArray(res.metadataObjects) ? res.metadataObjects : [ res.metadataObjects ];
      res.metadataObjects = _.map(res.metadataObjects, function(mo) {
        if (mo.childXmlNames) {
          mo.childXmlNames = _.isArray(mo.childXmlNames) ? mo.childXmlNames: [ mo.childXmlNames ];
        }
        mo.inFolder = mo.inFolder === 'true';
        mo.metaFile = mo.metaFile === 'true';
        return mo;
      });
      res.partialSaveAllowed = res.partialSaveAllowed === 'true';
      res.testRequired = res.testRequired === 'true';
      return res;
    }).thenCall(callback);
  };
  
  /**
   * @typedef {Object} Metadata~ListMetadataQuery
   * @prop {String} type - The metadata type, such as CustomObject, CustomField, or ApexClass
   * @prop {String} [folder] - The folder associated with the component.
   */
  
  /**
   * @typedef {Object} Metadata~FileProperties
   * @prop {String} type - The metadata type, such as CustomObject, CustomField, or ApexClass
   * @prop {String} createdById - ID of the user who created the file
   * @prop {String} createdByName - Name of the user who created the file
   * @prop {String} createdDate - Date and time when the file was created
   * @prop {String} fileName - Name of the file
   * @prop {String} fullName - The file developer name used as a unique identifier for API access
   * @prop {String} id - ID of the file
   * @prop {String} lastModifiedById - ID of the user who last modified the file
   * @prop {String} lastModifiedByName - Name of the user who last modified the file
   * @prop {String} lastModifiedDate - Date and time that the file was last modified
   * @prop {String} [manageableState] - Indicates the manageable state of the specified component if it is contained in a package
   * @prop {String} [namespacePrefix] - The namespace prefix of the component
   */
  
  /**
   * Retrieves property information about metadata components in your organization
   *
   * @param {Metadata~ListMetadataQuery|Array.<Metadata~ListMetadataQuery>} queries - The criteria object(s) specifing metadata to list
   * @param {String} [version] - The API version for which you want metadata; for example, 29.0
   * @param {Callback.<Array.<Metadata~FileProperties>>} [callback] - Callback function
   * @returns {Promise.<Array.<Metadata~FileProperties>>}
   */
  Metadata.prototype.list = function(queries, version, callback) {
    if (!_.isString(version)) {
      callback = version;
      version = this._conn.version;
    }
    if (!_.isArray(queries)) {
      queries = [ queries ];
    }
    return this._invoke("listMetadata", { queries: queries, asOfVersion: version }, callback);
  };
  
  /**
   * @typedef {Object} Metadata~RetrieveRequest
   */
  
  /**
   * Retrieves XML file representations of components in an organization
   *
   * @param {Metadata~RetrieveRequest} request - Options for determining which packages or files are retrieved
   * @param {Callback.<Metadata~AsyncResult>} [callback] - Callback function
   * @returns {Metadata~RetrieveResultLocator}
   */
  Metadata.prototype.retrieve = function(request, callback) {
    var res = this._invoke("retrieve", { request: request });
    return new RetrieveResultLocator(this, res).thenCall(callback);
  };
  
  /**
   * Checks the status of declarative metadata call retrieve() and returns the zip file contents
   *
   * @param {String} id - Async process id returned from previous retrieve request
   * @param {Callback.<Metadata~RetrieveResult>} [callback] - Callback function
   * @returns {Promise.<Metadata~RetrieveResult>}
   */
  Metadata.prototype.checkRetrieveStatus = function(id, callback) {
    return this._invoke("checkRetrieveStatus", { asyncProcessId: id }, callback);
  };
  
  /**
   * Deploy components into an organization using zipped file representations
   *
   * @param {stream.Stream|Buffer|String} zipInput - Zipped file input source in readable stream, binary buffer or Base64-encoded string
   * @param {Object} [options] - Options used in deployment
   * @param {Boolean} [options.allowMissingFiles] - Specifies whether a deploy succeeds even if files that are specified in package.xml but are not in the .zip file or not.
   * @param {Boolean} [options.autoUpdatePackage] - If a file is in the .zip file but not specified in package.xml, specifies whether the file should be automatically added to the package or not.
   * @param {Boolean} [options.checkOnly] - Indicates whether Apex classes and triggers are saved to the organization as part of the deployment (false) or not (true).
   * @param {Boolean} [options.ignoreWarnings] - Indicates whether a warning should allow a deployment to complete successfully (true) or not (false). Defaults to false.
   * @param {Boolean} [options.performRetrieve] - Indicates whether a retrieve() call is performed immediately after the deployment (true) or not (false).
   * @param {Boolean} [options.purgeOnDelete] - If true, the deleted components in the destructiveChanges.xml manifest file aren't stored in the Recycle Bin.
   * @param {Boolean} [options.rollbackOnError] - Indicates whether any failure causes a complete rollback (true) or not (false).
   * @param {Boolean} [options.runAllTests] - If true, all Apex tests defined in the organization are run.
   * @param {Array.<String>} [options.runTests] - A list of Apex tests to be run during deployment.
   * @param {Boolean} [options.singlePackage] - Indicates whether the specified .zip file points to a directory structure with a single package (true) or a set of packages (false).
   * @param {Callback.<Metadata~AsyncResult>} [callback] - Callback function
   * @returns {Metadata~DeployResultLocator}
   */
  Metadata.prototype.deploy = function(zipInput, options, callback) {
    if (!options || _.isFunction(options)) {
      callback = options;
      options = {};
    }
    var deferred = Promise.defer();
    if (_.isObject(zipInput) && _.isFunction(zipInput.pipe)) {
      var bufs = [];
      zipInput.on('data', function(d) {
        bufs.push(d);
      });
      zipInput.on('end', function() {
        deferred.resolve(Buffer.concat(bufs).toString('base64'));
      });
      // zipInput.resume();
    } else if (zipInput instanceof Buffer) {
      deferred.resolve(zipInput.toString('base64'));
    } else if (zipInput instanceof String || typeof zipInput === 'string') {
      deferred.resolve(zipInput);
    } else {
      throw "Unexpected zipInput type";
    }
  
    var self = this;
    var res = deferred.promise.then(function(zipContentB64) {
      return self._invoke("deploy", {
        ZipFile: zipContentB64,
        DeployOptions: options
      }, callback);
    });
    return new DeployResultLocator(this, res).thenCall(callback);
  };
  
  /**
   *
   * @param {String} validationId - Validation id returned from "check only" deployment
   * @param {Callback.<Metadata~DeployResult>} [callback] - Callback function
   * @returns {Promise.<Metadata~DeployResult>}
   */
  Metadata.prototype.deployRecentValidation = function(validationId, callback) {
    var res = this._invoke("deployRecentValidation", { validationId: validationId });
    return new DeployResultLocator(this, res).thenCall(callback);
  };
  
  /**
   * Checks the status of declarative metadata call deploy()
   *
   * @param {String} id - Async process id returned from previous deploy request
   * @param {Boolean} [includeDetails] - Sets the DeployResult object to include details information (default: false)
   * @param {Callback.<Metadata~DeployResult>} [callback] - Callback function
   * @returns {Promise.<Metadata~DeployResult>}
   */
  Metadata.prototype.checkDeployStatus = function(id, includeDetails, callback) {
    if (_.isObject(includeDetails) || _.isBoolean(includeDetails)) {
      includeDetails = !!includeDetails;
    } else {
      callback = includeDetails;
      includeDetails = false;
    }
    return this._invoke("checkDeployStatus", {
      asyncProcessId: id,
      includeDetails : includeDetails
    }).then(function(res) {
      res.done = res.done === 'true';
      res.success = res.success === 'true';
      res.checkOnly = res.checkOnly === 'true';
      res.runTestsEnabled = res.runTestsEnabled === 'true';
      if (res.ignoreWarnings) {
        res.ignoreWarnings = res.ignoreWarnings === 'true';
      }
      if (res.rollbackOnError) {
        res.rollbackOnError = res.rollbackOnError === 'true';
      }
      res.numberComponentErrors = Number(res.numberComponentErrors);
      res.numberComponentsDeployed = Number(res.numberComponentsDeployed);
      res.numberComponentsTotal = Number(res.numberComponentsTotal);
      res.numberTestErrors = Number(res.numberTestErrors);
      res.numberTestsCompleted = Number(res.numberTestsCompleted);
      res.numberTestsTotal = Number(res.numberTestsTotal);
  
      return res;
    }).thenCall(callback);
  };
  
  
  /*--------------------------------------------*/
  
  /**
   * @typedef {Object} Metadata~AsyncResult
   * @prop {Boolean} done - Indicates whether the call has completed or not
   * @prop {String} id - ID of the component being created, updated, deleted, deployed, or retrieved
   * @prop {String} state - The state four possible values: Queued, InProgress, Completed, and Error.
   * @prop {String} [statusCode] - If an error occurred during the create(), update(), or delete() call, a status code is returned
   * @prop {String} [message] - Message corresponding to the statusCode field returned
   */
  
  /**
   * The locator class for Metadata API asynchronous call result
   *
   * @protected
   * @class Metadata~AsyncResultLocator
   * @extends events.EventEmitter
   * @implements Promise.<Metadata~AsyncResult|Array.<Metadata~AsyncResult>>
   * @param {Metadata} meta - Metadata API object
   * @param {Promise.<Metadata~AsyncResult|Array.<Metadata~AsyncResult>>} results - Promise object for async result info
   * @param {Boolean} [isArray] - Indicates whether the async request is given in array or single object
   */
  var AsyncResultLocator = function(meta, results, isArray) {
    this._meta = meta;
    this._results = results;
    this._isArray = isArray;
  };
  
  inherits(AsyncResultLocator, events.EventEmitter);
  
  /**
   * Promise/A+ interface
   * http://promises-aplus.github.io/promises-spec/
   *
   * Delegate to deferred promise, return promise instance for batch result
   *
   * @method Metadata~AsyncResultLocator#then
   */
  AsyncResultLocator.prototype.then = function(onResolve, onReject) {
    var self = this;
    return this._results.then(function(results) {
      var convertType = function(res) {
        if (res.$ && res.$["xsi:nil"] === 'true') {
          return null;
        }
        res.done = res.done === 'true';
        return res;
      };
      results = _.isArray(results) ? _.map(results, convertType) : convertType(results);
      if (self._isArray && !_.isArray(results)) {
        results = [ results ];
      }
      return onResolve(results);
    }, onReject);
  };
  
  /**
   * Promise/A+ extension
   * Call "then" using given node-style callback function
   *
   * @method Metadata~AsyncResultLocator#thenCall
   */
  AsyncResultLocator.prototype.thenCall = function(callback) {
    return _.isFunction(callback) ? this.then(function(res) {
      process.nextTick(function() {
        callback(null, res);
      });
    }, function(err) {
      process.nextTick(function() {
        callback(err);
      });
    }) : this;
  };
  
  /**
   * Check the status of async request
   *
   * @method Metadata~AsyncResultLocator#check
   * @param {Callback.<Metadata~AsyncResult|Array.<Metadata~AsyncResult>>} [callback] - Callback function
   * @returns {Promise.<Metadata~AsyncResult|Array.<Metadata~AsyncResult>>}
   */
  AsyncResultLocator.prototype.check = function(callback) {
    var self = this;
    var meta = this._meta;
    return this.then(function(results) {
      var ids = _.isArray(results) ? _.map(results, function(res){ return res.id; }) : results.id;
      self._ids = ids;
      return meta.checkStatus(ids);
    }).thenCall(callback);
  };
  
  /**
   * Polling until async call status becomes complete or error
   *
   * @method Metadata~AsyncResultLocator#poll
   * @param {Number} interval - Polling interval in milliseconds
   * @param {Number} timeout - Polling timeout in milliseconds
   */
  AsyncResultLocator.prototype.poll = function(interval, timeout) {
    var self = this;
    var startTime = new Date().getTime();
    var poll = function() {
      var now = new Date().getTime();
      if (startTime + timeout < now) {
        var errMsg = "Polling time out.";
        if (self._ids) {
          errMsg += " Process Id = " + self._ids;
        }
        self.emit('error', new Error(errMsg));
        return;
      }
      self.check().then(function(results) {
        var done = true;
        var resultArr = _.isArray(results) ? results : [ results ];
        for (var i=0, len=resultArr.length; i<len; i++) {
          var result = resultArr[i];
          if (result && !result.done) {
            self.emit('progress', result);
            done = false;
          }
        }
        if (done) {
          self.emit('complete', results);
        } else {
          setTimeout(poll, interval);
        }
      }, function(err) {
        self.emit('error', err);
      });
    };
    setTimeout(poll, interval);
  };
  
  /**
   * Check and wait until the async requests become in completed status
   *
   * @method Metadata~AsyncResultLocator#complete
   * @param {Callback.<Metadata~AsyncResult|Array.<Metadata~AsyncResult>>} [callback] - Callback function
   * @returns {Promise.<Metadata~AsyncResult|Array.<Metadata~AsyncResult>>}
   */
  AsyncResultLocator.prototype.complete = function(callback) {
    var deferred = Promise.defer();
    this.on('complete', function(results) {
      deferred.resolve(results);
    });
    this.on('error', function(err) {
      deferred.reject(err);
    });
    var meta = this._meta;
    this.poll(meta.pollInterval, meta.pollTimeout);
    return deferred.promise.thenCall(callback);
  };
  
  /*--------------------------------------------*/
  /**
   * The locator class to track retreive() Metadata API call result
   *
   * @protected
   * @class Metadata~RetrieveResultLocator
   * @extends Metadata~AsyncResultLocator
   * @param {Metadata} meta - Metadata API object
   * @param {Promise.<Metadata~AsyncResult>} result - Promise object for async result of retrieve call()
   */
  var RetrieveResultLocator = function(meta, result) {
    RetrieveResultLocator.super_.call(this, meta, result);
  };
  
  inherits(RetrieveResultLocator, AsyncResultLocator);
  
  /**
   * @typedef {Object} Metadata~RetrieveResult
   * @prop {Array.<Metadata~FileProperties>} fileProperties - Contains information about the properties of each component in the .zip file, and the manifest file package.xml
   * @prop {String} id - ID of the component being retrieved
   * @prop {Array.<Object>} messages - Contains information about the success or failure of the retrieve() call
   * @prop {String} zipFile - The zip file returned by the retrieve request. Base 64-encoded binary data
   */
  
  /**
   * Check and wait until the async request becomes in completed status,
   * and retrieve the result data.
   *
   * @memthod Metadata~RetrieveResultLocator#complete
   * @param {Callback.<Metadata~RetrieveResult>} [callback] - Callback function
   * @returns {Promise.<Metadata~RetrieveResult>}
   */
  RetrieveResultLocator.prototype.complete = function(callback) {
    var meta = this._meta;
    return RetrieveResultLocator.super_.prototype.complete.call(this).then(function(result) {
      return meta.checkRetrieveStatus(result.id);
    }).thenCall(callback);
  };
  
  /**
   * Change the retrieved result to Node.js readable stream
   *
   * @method Metadata~RetrieveResultLocator#stream
   * @returns {stream.Readable}
   */
  RetrieveResultLocator.prototype.stream = function() {
    var self = this;
    var resultStream = new stream.Readable();
    var reading = false;
    resultStream._read = function() {
      if (reading) { return; }
      reading = true;
      self.complete(function(err, result) {
        if (err) {
          resultStream.emit('error', err);
        } else {
          resultStream.push(new Buffer(result.zipFile, 'base64'));
          resultStream.push(null);
        }
      });
    };
    return resultStream;
  };
  
  /*--------------------------------------------*/
  /**
   * The locator class to track deploy() Metadata API call result
   *
   * @protected
   * @class Metadata~DeployResultLocator
   * @extends Metadata~AsyncResultLocator
   * @param {Metadata} meta - Metadata API object
   * @param {Promise.<Metadata~AsyncResult>} result - Promise object for async result of deploy() call
   */
  var DeployResultLocator = function(meta, result) {
    DeployResultLocator.super_.call(this, meta, result);
  };
  
  inherits(DeployResultLocator, AsyncResultLocator);
  
  /**
   * @typedef {Object} Metadata~DeployResult
   * @prop {String} id - ID of the component being deployed
   * @prop {Boolean} checkOnly - Indicates whether this deployment is being used to check the validity of the deployed files without making any changes in the organization or not
   * @prop {String} completedDate - Timestamp for when the deployment process ended
   * @prop {String} createdDate - Timestamp for when the deploy() call was received
   * @prop {Array.<Object>} [details] - Provides the details of a deployment that is in-progress or ended, if includeDetails is set to true in checkDeployStatus() call
   * @prop {Boolean} done - Indicates whether the server finished processing the deploy() call for the specified id
   * @prop {String} [errorMessage] - Message corresponding to the values in the errorStatusCode field
   * @prop {String} [errorStatusCode] - If an error occurred during the deploy() call, a status code is returned, and the message corresponding to the status code is returned in the errorMessagefield
   * @prop {Boolean} [ignoreWarnings] - Specifies whether a deployment should continue even if the deployment generates warnings
   * @prop {String} lastModifiedDate - Timestamp of the last update for the deployment process
   * @prop {Number} numberComponentErrors - The number of components that generated errors during this deployment
   * @prop {Number} numberComponentsDeployed - The number of components deployed in the deployment process
   * @prop {Number} numberComponentsTotal - The total number of components in the deployment
   * @prop {Number} numberTestErrors - The number of Apex tests that have generated errors during this deployment
   * @prop {Number} numberTestsCompleted - The number of completedApex tests for this deployment
   * @prop {Number} numberTestsTotal - The total number of Apex tests for this deployment
   * @prop {Boolean} [rollbackOnError] - Indicates whether any failure causes a complete rollback or not. Default is true.
   * @prop {String} startDate - Timestamp for when the deployment process began
   * @prop {String} status - Indicates the current state of the deployment
   * @prop {Boolean} success - Indicates whether the deployment was successful or not
   */
  
  /**
   * Check and wait until the async request becomes in completed status,
   * and retrieve the result data.
   *
   * @method Metadata~DeployResultLocator#complete
   * @param {Callback.<Metadata~DeployResult>} [callback] - Callback function
   * @returns {Promise.<Metadata~DeployResult>}
   */
  DeployResultLocator.prototype.complete = function(includeDetails, callback) {
    if (_.isFunction(includeDetails)) {
      callback = includeDetails;
      includeDetails = false;
    }
    var meta = this._meta;
    return DeployResultLocator.super_.prototype.complete.call(this).then(function(result) {
      return meta.checkDeployStatus(result.id, includeDetails);
    }).thenCall(callback);
  };
  
  
  /*--------------------------------------------*/
  /*
   * Register hook in connection instantiation for dynamically adding this API module features
   */
  jsforce.on('connection:new', function(conn) {
    conn.metadata = new Metadata(conn);
  });
  