zanata/project.js

'use strict';

/**
 * This is the Project Management API for Zanata
 * @exports zanata/project
 *
 * @requires underscore
 * @requires co
 * @requires util
 * @requires fs
 * @requires path
 * @requires events
 * @requires glob
 * @requires xdg
 * @requires ssl-root-cas
 * @requires zanata/api/projectsResource
 * @requires zanata/api/projectResource
 * @requires zanata/api/projectLocalesResource
 * @requires zanata/api/projectIterationResource
 * @requires zanata/api/projectIterationLocalesResource
 * @requires zanata/api/sourceDocResource
 * @requires zanata/api/translatedDocResource
 * @requires zanata/api/statisticsResource
 * @requires zanata/fileMapping
 * @requires zanata/config
 * @requires zanata/gettext
 * @requires zanata/etag
 * @requires zanata/etagCache
 */
const _ = require('underscore');
const co = require('co');
const util = require('util');
const fs = require('fs');
const path = require('path');
const EventEmitter = require('events').EventEmitter;
const Glob = require('glob').Glob;
const xdg = require('xdg').basedir;
const ssl = require('ssl-root-cas').inject();
const debuglog = util.debuglog('zanata');

// read certificates before loading APIs
// otherwise it won't be applied.
require('glob').sync(path.join(xdg.configHome(), 'zanata-js', 'certs', '*.@(pem|crt)'),
          {nodir: true, dot: false})
  .forEach(function(v) {
    debuglog('Loading ' + v);
    ssl.addFile(v);
  });

const psr = require('./api/projectsResource.js').ProjectsResource;
const pr = require('./api/projectResource.js').ProjectResource;
const plr = require('./api/projectLocalesResource.js').ProjectLocalesResource;
const pir = require('./api/projectIterationResource.js').ProjectIterationResource;
const pilr = require('./api/projectIterationLocalesResource.js').ProjectIterationLocalesResource;
const sdr = require('./api/sourceDocResource.js').SourceDocResource;
const tdr = require('./api/translatedDocResource.js').TranslatedDocResource;
const sr = require('./api/statisticsResource.js').StatisticsResource;
const fmr = require('./fileMapping.js').FileMappingRule;
const em = require('../util/errmsg.js');
const yorn = require('../util/y-or-n.js');
const Config = require('./config.js').Config;
const Gettext = require('./gettext.js').Gettext;
const ETag = require('./etag.js');
const ETagCache = require('./etagCache.js');
const errmsg = em.errmsg;
const warnmsg = em.warnmsg;

/**
 * @class
 * @classdesc The base class to deal with the operations on the client side to the Zanata.
 * @extends EventEmitter
 */
class Project extends EventEmitter {

  /**
   * All of responses from the server will be sent through the events.
   *
   * @constructor
   * @see {@link module:zanata/config~Config}
   * @param {Object} param - The options for Project. most of the properties will be stored into {@link module:zanata/config~Config}.
   * @param {string} param.url - The URL to the Zanata server where you want to connect to.
   * @param {string} [param.username] - The username you want to connect to the Zanata with.
   * @param {string} [param.api-key] - The API key may be required to authorize for certain access on the Zanata.
   */
  constructor(params) {
    super();
    let configFile = params && params['project-config'];
    let self = this;
    self.params = params;
    self.config = new Config(configFile);
    if (params instanceof Object) {
      Object.keys(params).forEach(function(k) {
        if (params[k] != undefined)
          self.config.set(k, params[k]);
      });
    }
  }

  /**
   * Obtain the projects and the information available on the Zanata
   *
   * @param {string[]} [filter=['id', 'name', 'status']] - The properties you want to obtain from the project.
   *
   * @fires module:zanata/project~Project#fail
   * @fires module:zanata/project~Project#data_list
   */
  list(filter) {
    let p = new psr(this.config.get('config.url'));
    p
      .setAuthUser(this.config.get('username'))
      .setAuthToken(this.config.get('api-key'))
      .get()
      .then((d) => {
        let resp = d.response;
        let data = d.json;

        if (resp.statusCode !== 200)
          throw new Error(errmsg(p.httpStatusMessage(resp, data)));

        let delobj = Object.keys(data[0]).filter((v) => (filter.indexOf(v) < 0));
        data.forEach((o) => {
          delobj.forEach((v) => delete o[v]);
        });
        /**
         * data event from list method
         *
         * @event module:zanata/project~Project#data_list
         * @type {object[]}
         * @property {string} id - the project id
         * @property {string} defaultType - the default project type
         * @property {string} name - the project name
         * @property {string} status - the project status
         * @property {string} description - the project description
         * @property {string} sourceViewURL - the source view URL for the project
         * @property {string} sourceCheckoutURL - the source URL to check out
         *
         * @see {@link https://zanata.ci.cloudbees.com/job/zanata-api-site/site/zanata-common-api/rest-api-docs/json_Project.html|Project data type}
         */
        this.emit('data_list', data);
      })
    /**
     * fail event
     *
     * @event module:zanata/project~Project#fail
     * @type {Error}
     */
      .catch((e) => this.emit('fail', e));
    return this;
  }

  /**
   * Create a project
   *
   * @param {object} [params] - the parameters to create a project. most properties will be stored into {@link module:zanata/config~Config}.
   * @param {string} params.project - the project id
   * @param {string} params.project-name - the project name
   * @param {string} params.project-type - the default project type. it must be one of 'File', 'Gettext', 'Podir', 'Properties', 'Utf8Properties', 'Xliff', or 'Xml'
   * @param {string} [params.description] - the project description
   *
   * @see {@link module:zanata/config~Config}
   *
   * @fires module:zanata/project~Project#fail
   * @fires module:zanata/project~Project#data_create
   */
  create(params) {
    let projectTypes = ['File', 'Gettext', 'Podir', 'Properties', 'Utf8Properties', 'Xliff', 'Xml'];
    let self = this;
    let config = util._extend(new Config(), self.config);
    Object.keys(params).forEach((k) => {
      if (params[k] != undefined)
        config.set(k, params[k]);
    });
    if (config.get('project') == undefined) {
      self.emit('fail', new Error('No project id to create'));
    } else if (config.get('project-name') == undefined) {
      self.emit('fail', new Error('No project name to create'));
    } else if (config.get('project-type') == undefined) {
      self.emit('fail', new Error('No default project type to create'));
    } else if (projectTypes.map(v => v.toLowerCase()).indexOf(config.get('project-type').toLowerCase()) < 0) {
      self.emit('fail', new Error(util.format('Invalid default project type. must be one of %s but: %s',
                                              projectTypes.join(', '),
                                              config.get('project-type'))));
    } else {
      let p = new pr(self.config.get('config.url'));
      p
        .setAuthUser(self.config.get('username'))
        .setAuthToken(self.config.get('api-key'))
        .put({id: config.get('project'),
              defaultType: config.get('project-type'),
              name: config.get('project-name'),
              description: config.get('description')})
        .then((d) => {
          let resp = d.response;
          let data = d.json || d.data;

          if (resp.statusCode === 200) {
            return 'Successfully updated';
          } else if (resp.statusCode === 201) {
            return 'Successfully created';
          } else if (resp.statusCode === 401) {
            throw new Error('Unauthorized operation');
//          } else if (resp.statusCode === 403) {
          } else {
            throw new Error(errmsg(p.httpStatusMessage(resp, data)));
          }
        })
      /**
       * data event from create method. this contains the result string.
       *
       * @event module:zanata/project~Project#data_create
       * @type {string}
       */
        .then(v => this.emit('data_create', v))
        .catch(e => self.emit('fail', e));
    }
    return self;
  }

  /**
   * Obtain the project information from the Zanata.
   *
   * @param {string} id - the project id to obtain the information
   * @param {boolean} containLocales - true to contain the locale information in the result, otherwise will be suppressed.
   *
   * @fires module:zanata/project~Project#fail
   * @fires module:zanata/project~Project#data_info
   */
  info(id, containLocales) {
    let self = this;
    let p = new pr(self.config.get('config.url'));
    p
      .setAuthUser(self.config.get('username'))
      .setAuthToken(self.config.get('api-key'))
      .get(id)
      .then((d) => {
        let resp = d.response;
        let data = d.json || d.data;
        let fLocales;

        if (resp.statusCode === 404) {
          throw new Error('No such projects on the server: ' + id);
        } else if (resp.statusCode !== 200) {
          throw new Error(errmsg(p.httpStatusMessage(resp, data)));
        }

        if (containLocales) {
          fLocales = () => {
            return new plr(self.config.get('config.url'))
              .get(id)
              .then((dd) => {
                let resp = dd.response;
                let data = dd.json || dd.data;
                if (resp.statusCode === 404) {
                  throw new Error('No such projects on the server: ' + id);
                } else if (resp.statusCode !== 200) {
                  throw new Error(errmsg(dd.self.httpStatusMessage(resp, dd.data)));
                }
                return data;
              });
          };
        } else {
          fLocales = () => Promise.resolve(null);
        }
        if (data.iterations) {
          let cb = data.iterations.filter((o)=>o.status === 'ACTIVE').map((o) => {
            let pi = new pir(self.config.get('config.url'));
            // XXX: should check href and type?
            return pi
              .setAuthUser(self.config.get('username'))
              .setAuthToken(self.config.get('api-key'))
              .get(id, o.id)
              .then((d) => {
                let resp = d.response;
                let data = d.json || d.data;

                if (resp.statusCode === 404) {
                  // ignore this error
                  return null;
                } else if (resp.statusCode === 200) {
                  return data;
                } else {
                  throw new Error(errmsg(pi.httpStatusMessage(resp, data)));
                }
              });
          });
          Promise.all(cb)
            .then((v) => {
              delete data.iterations;
              data.versions = v.filter((x) => (x != undefined));
              return fLocales();
            })
            .then((v) => {
              data.locales = v;
              self.emit('data_info', data);
            })
            .catch((e) => self.emit('fail', e));
        } else {
          /**
           * data event from info method.
           *
           * @event module:zanata/project~Project#data_info
           * @type {object}
           * @property {string} id - the project id
           * @property {string} defaultType - the default project type
           * @property {string} name - the project name
           * @property {string} status - the project status
           * @property {string} description - the project description
           * @property {string} sourceViewURL - the source view URL for the project
           * @property {string} sourceCheckoutURL - the source URL to check out
           * @property {object[]} versions - the version-related information
           * @property {string} versions.id - the version id
           * @property {string} versions.defaultType - the project version type
           * @property {string} versions.status - the status
           * @property {object[]} [locales] - the locale name available in this project
           * @property {object} locales.localeId - the LocaleId object
           * @property {string} locales.localeId.id - BCP-47 language tag
           * @property {string} [locales.displayName] - the display name of the locale
           * @property {string} [locales.alias] - the alias name
           * @property {string} [locales.nativeName]
           * @property {boolean} locales.enabled
           * @property {boolean} locales.enabledByDefault
           * @property {string} [locales.pluralForms]
           *
           * @see {@link https://zanata.ci.cloudbees.com/job/zanata-api-site/site/zanata-common-api/rest-api-docs/json_Project.html|Project data type}
           * @see {@link https://zanata.ci.cloudbees.com/job/zanata-api-site/site/zanata-common-api/rest-api-docs/json_LocaleId.html|LocaleId data type}
           * @see {@link https://zanata.ci.cloudbees.com/job/zanata-api-site/site/zanata-common-api/rest-api-docs/json_LocaleDetails.html|LocaleDetails data type}
           */
          fLocales()
            .then((v) => {
              data.locales = v;
              self.emit('data_info', data);
            })
            .catch((e) => self.emit('fail', e));
        }
      });
    return self;
  }

  /**
   * Create a version in the project.
   *
   * @param {string} projectId - the project id
   * @param {string} versionId - the version id
   * @param {object} params - the parameters to create a version. most properties will be stored into {@link module:zanata/config~Config}.
   * @param {string} params.project-type - the default project type. it must be one of 'File', 'Gettext', 'Podir', 'Properties', 'Utf8Properties', 'Xliff', or 'Xml'
   *
   * @see {@link module:zanata/config~Config}
   *
   * @fires module:zanata/project~Project#fail
   * @fires module:zanata/project~Project#data_create
   */
  createVersion(projectId, versionId, params) {
    let self = this;
    let pi = new pir(self.config.get('config.url'));
    let config = util._extend(new Config(), self.config);
    if (params == undefined)
      params = {};
    Object.keys(params).forEach((k) => {
      if (params[k] != undefined)
        config.set(k, params[k]);
    });
    if (projectId == undefined)
      projectId = config.get('project');
    pi
      .setAuthUser(self.config.get('username'))
      .setAuthToken(self.config.get('api-key'))
      .get(projectId, versionId)
      .then((d) => {
        let resp = d.response;
        let data = d.json || d.data;

        if (resp.statusCode === 200)
          throw new Error(util.format("Version '%s' already exists", versionId));
        else if (resp.statusCode !== 404)
          throw new Error(errmsg(pi.httpStatusMessage(resp, data)));

        return pi
          .put(projectId,
               versionId,
               {id: versionId, status: 'ACTIVE', projectType: self.config.get('project-type')})
          .then((d) => {
            let resp = d.response;
            let data = d.json || d.data;

            if (resp.statusCode === 200)
              throw new Error(util.format("Version '%s' already exists", versionId));
            else if (resp.statusCode === 201)
              self.emit('data_create', 'Successfully created');
            else if (resp.statusCode === 404)
              throw new Error(util.format("Project '%s' does not exist", projectId));
            else if (resp.statusCode === 401)
              throw new Error('Unauthorized operation');
            else
              throw new Error(errmsg(pi.httpStatusMessage(resp, data)));
          });
      })
      .catch((e) => self.emit('fail', e));

    return self;
  }

  /**
   * Obtain the information about the version for the project
   *
   * @param {string} projectId - the project id
   * @param {string} versionId - the version id
   * @param {boolean} containLocales - true to contain the locale information in the result, otherwise will be suppressed.
   *
   * @fires module:zanata/project~Project#fail
   * @fires module:zanata/project~Project#data_version_info
   */
  versionInfo(projectId, versionId, containLocales) {
    let self = this;
    let p = new pr(self.config.get('config.url'));
    if (projectId == undefined)
      projectId = self.config.get('project');
    p
      .setAuthUser(self.config.get('username'))
      .setAuthToken(self.config.get('api-key'))
      .get(projectId)
      .then((d) => {
        let resp = d.response;

        if (resp.statusCode < 200 || resp.statusCode >= 300)
          throw new Error(errmsg(p.httpStatusMessage(resp, d.data)));

        let data = d.json;

        if (!data.iterations) {
          // if no locales available, return an empty array.
          return {data: []};
          // self.emit('data', []);
        }

        let avail = data.iterations.map((o) => o.id);
        let ret = data.iterations.filter((o) => (o.id === versionId));

        if (ret.length === 0)
          throw new Error(util.format('No such version available in %s: %s\nAvailable versions are: %s',
                                      projectId, versionId, avail.join(', ')));
        // duplicate version shouldn't be available
        ret = ret[0];

        return {version: ret.id};
      })
      .then((d) => {
        if (d.data)
          return d.data;
        let pi = new pir(self.config.get('config.url'));
        // XXX: should check href and type?
        return pi
          .setAuthUser(self.config.get('username'))
          .setAuthToken(self.config.get('api-key'))
          .get(projectId, d.version)
          .then((dd) => {
            let resp = dd.response;
            let json = dd.json;

            if (resp.statusCode < 200 || resp.statusCode >= 300)
              throw new Error(errmsg(pi.httpStatusMessage(resp, dd.data)));

            if (containLocales) {
              let pil = new pilr(self.config.get('config.url'));
              return pil
                .setAuthUser(self.config.get('username'))
                .setAuthToken(self.config.get('api-key'))
                .get(projectId, d.version)
                .then((ddd) => {
                  let resp = ddd.response;
                  if (resp.statusCode >= 200 && resp.statusCode < 300) {
                    json.locales = ddd.json;
                    return json;
                  } else {
                    throw new Error(errmsg(pil.httpStatusMessage(resp, ddd.data)));
                  }
                });
            } else {
              return json;
            }
          });
      })
    /**
     * data event from versionInfo method.
     *
     * @event module:zanata/project~Project#data_version_info
     * @type {object}
     * @property {string} id - the version id
     * @property {string} status - the status
     * @property {string} projectType - the project type
     * @property {object[]} [locales] - the locale name available in this project
     * @property {object} locales.localeId - the LocaleId object
     * @property {string} locales.localeId.id - BCP-47 language tag
     * @property {string} [locales.displayName] - the display name of the locale
     * @property {string} [locales.alias] - the alias name
     * @property {string} [locales.nativeName]
     * @property {boolean} locales.enabled
     * @property {boolean} locales.enabledByDefault
     * @property {string} [locales.pluralForms]
     *
     * @see {@link https://zanata.ci.cloudbees.com/job/zanata-api-site/site/zanata-common-api/rest-api-docs/json_ProjectIteration.html|ProjectIteration data type}
     * @see {@link https://zanata.ci.cloudbees.com/job/zanata-api-site/site/zanata-common-api/rest-api-docs/json_LocaleId.html|LocaleId data type}
     * @see {@link https://zanata.ci.cloudbees.com/job/zanata-api-site/site/zanata-common-api/rest-api-docs/json_LocaleDetails.html|LocaleDetails data type}
     */
      .then((v) => self.emit('data_version_info', v))
      .catch((e) => self.emit('fail', e));

    return self;
  }

  __pullSourceCb(projectId, versionId, docId, etag, params, callback) {
    let self = this;
    let sd = new sdr(self.config.get('config.url'));
    if (projectId == undefined)
      projectId = self.config.get('project');
    if (versionId == undefined)
      versionId = self.config.get('project-version');
    let verbose = params && params.verbose || 0;
    let force = params && params.force || false;
    let label;
    let payload;

    if (!force) {
      etag.find(docId, versionId, 'en', (cache) => {
        if (cache) {
          sd.setETag(cache.getServerETag());
          payload = cache.getPayload();
        }
      });
    }
    if (verbose > 2)
      label = util.format('Downloading %s', docId);
    if (label)
      console.time(label);
    sd
      .setAuthUser(self.config.get('username'))
      .setAuthToken(self.config.get('api-key'))
      .getDoc(projectId, versionId, docId)
      .then((d) => {
        let resp = d.response;
        let data = d.json || d.data;
        let updateETag = false;

        if (resp.statusCode === 304)
          self.emit('warning', 'Source for ' + docId + " wasn't modified. using the cache");
        else if (resp.statusCode !== 200)
          throw new Error(errmsg(sd.httpStatusMessage(resp, data)));
        else
          updateETag = true;

        if (label)
          console.timeEnd(label);
        if (updateETag) {
          etag
            .add(new ETagCache()
                 .setDocId(docId)
                 .setVersionId(versionId)
                 .setLanguage('en')
                 .setPayload(data)
                 .setServerETag(sd.getETag()))
            .save();
          payload = data;
        }
        callback(null, payload);
      })
      .catch((e) => callback(e));

    return self;
  }

  _pullSourceCb(projectId, versionId, docId, etag, params, callback) {
    this.__pullSourceCb(projectId, versionId, docId, etag, params, (e, d) => {
      if (e) {
        callback(e);
      } else {
        Gettext.json2po(d)
          .then((data) => callback(null, {name: docId, type: 'pot', data: data}))
          .catch((e) => callback(e));
      }
    });
    return this;
  }

  /**
   * Pull the source document from the Zanata
   *
   * @param {string} projectId - the project id
   * @param {string} versionId - the version id
   * @param {string} docId - the document id
   * @param {object} [params] - the parameters to pull the source document
   * @param {string} [params.potdir='./po'] - the place where store the cache file
   * @param {number} [params.verbose=false] - Show more progress messages verbosely.
   * @param {boolean} [params.force=false] - Transfer the document without the cache
   *
   * @fires module:zanata/project~Project#fail
   * @fires module:zanata/project~Project#warning
   * @fires module:zanata/project~Project#data_pull
   * @fires module:zanata/project~Project#end_pull
   */
  pullSource(projectId, versionId, docId, params) {
    let self = this;
    let potdir = params && params.potdir || './po';
    let etag = new ETag(path.join(potdir, 'etag-cache.json'));

    self._pullSourceCb(projectId, versionId, docId, etag, params, (e, d) => {
      if (e) {
        self.emit('fail', e);
      } else {
        self.emit('data_pull', d);
        self.emit('end_pull', [{name: docId, type: 'pot', pulled: true}]); // for compatibility to other APIs
      }
    });
    return self;
  }

  /**
   * warning event from pull/push methods. this contains the warning messages where happened during processing
   * and not that important more than stopping.
   *
   * @event module:zanata/project~Project#warning
   * @type {string}
   */

  /**
   * data event from pull methods. this is emitted when any data is received from the Zanata on pulling.
   *
   * @event module:zanata/project~Project#data_pull
   * @type {object}
   * @property {string} name - the document id
   * @property {string} type - the document type. 'pot' for the source document and 'po' for the translation
   * @property {string} data - the contents for POT file
   * @property {string} [locale] - the locale name for document. this is only available when pulling translations
   */

  /**
   * end event for pull methods. this is emitted when all of pulling is finished.
   *
   * @event module:zanata/project~Project#end_pull
   * @type {object[]}
   * @property {string} name - the document id
   * @property {string} type - the document type. 'pot' for the source document and 'po' for the translation
   * @property {boolean} pulled - whether the document was actually pulled from the Zanata or the cache due to no changes since the last pull.
   * @property {string} [locale] - the locale name for document. this is only available when pulling translations
   */

  /**
   * Pull the source documents from the Zanata
   *
   * @param {string} projectId - the project id
   * @param {string} versionId - the project version id
   * @param {object} params - the parameters to pull the source documents from the Zanata
   * @param {string} [params.potdir='./po'] - the place where store the cache file
   * @param {number} [params.verbose=false] - Show more progress messages verbosely.
   * @param {boolean} [params.force=false] - Transfer the document without the cache
   *
   * @fires module:zanata/project~Project#fail
   * @fires module:zanata/project~Project#warning
   * @fires module:zanata/project~Project#data_pull
   * @fires module:zanata/project~Project#end_pull
   */
  pullSources(projectId, versionId, params) {
    let self = this;
    let potdir = params && params.potdir || './po';
    let etag = new ETag(path.join(potdir, 'etag-cache.json'));

    if (projectId == undefined)
      projectId = self.config.get('project');
    if (versionId == undefined)
      versionId = self.config.get('project-version');

    let sd = new sdr(self.config.get('config.url'))
        .setAuthUser(self.config.get('username'))
        .setAuthToken(self.config.get('api-key'))
        .get(projectId, versionId)
        .then((d) => {
          let resp = d.response;
          let data = d.json || d.data;

          if (resp.statusCode === 404)
            throw new Error('No such resources available on the server: [project: ' + projectId + ', version: ' + versionId + ']');
          else if (resp.statusCode !== 200)
            throw new Error(errmsg(sd.httpStatusMessage(resp, data)));

          // contains document data in Array
          let promises = data.map((o) => {
            return new Promise((resolve, reject) => {
              self._pullSourceCb(projectId, versionId, o.name, etag, params, (e, d) => {
                if (e)
                  reject(e);
                else
                  self.emit('data_pull', d);
                  resolve(o);
              });
            });
          });
          return Promise.all(promises)
            .then((d) => d.map((v) => {
              return {name: v.name, type: 'pot', pulled: true};
            }))
            .catch((e) => self.emit('fail', e));
        })
        .then((d) => self.emit('end_pull', d))
        .catch((e) => self.emit('fail', e));

    return self;
  }

  _pullTranslationCb(projectId, versionId, docId, potObj, locale, etag, params, callback) {
    let self = this;
    if (projectId == undefined)
      projectId = self.config.get('project');
    if (versionId == undefined)
      versionId = self.config.get('project-version');
    let verbose = params && params.verbose || 0;
    let skeletons = params && params.skeletons || false;
    let force = params && params.force || false;
    let label;
    let payload;
    let td = new tdr(self.config.get('config.url'));

    if (!force) {
      etag.find(docId, versionId, locale, (cache) => {
        if (cache) {
          td.setETag(cache.getServerETag());
          payload = cache.getPayload();
        }
      });
    }
    if (verbose > 2)
      label = util.format('Downloading %s translation for %s', locale, docId);
    if (label)
      console.time(label);
    td
      .setAuthUser(self.config.get('username'))
      .setAuthToken(self.config.get('api-key'))
      .get(projectId, versionId, docId, locale, skeletons)
      .then((d) => {
        let resp = d.response;
        let data = d.json || d.data;

        if (resp.statusCode === 304) {
          self.emit('warning', locale + ' translation for ' + docId + " wasn't modified. using the cache");
          data = payload;
          payload = null;
        } else if (resp.statusCode === 404) {
          self.emit('warning', 'Missing ' + locale + ' translation for ' + docId);
          data = null;
        } else if (resp.statusCode !== 200) {
          throw new Error(errmsg(td.httpStatusMessage(resp, data)));
        } else {
          payload = data;
        }
        if (data) {
          if (label)
            console.timeEnd(label);
          Object.keys(data).forEach((k) => potObj[k] = data[k]);
          Gettext.json2po(potObj)
            .then((dd) => {
              if (payload) {
                etag
                  .add(new ETagCache()
                       .setDocId(docId)
                       .setVersionId(versionId)
                       .setLanguage(locale)
                       .setPayload(payload)
                       .setServerETag(td.getETag()))
                  .save();
              }
              callback(null, {name: docId, type: 'po', locale: locale, data: dd, pulled: true});
            })
            .catch((ee) => callback(ee));
        } else {
          callback();
        }
      })
      .catch((e) => callback(e));

    return self;
  }

  /**
   * Pull the translated document from the Zanata
   *
   * @param {string} projectId - the project id
   * @param {string} versionId - the project version id
   * @param {string} docId - the document id
   * @param {string[]|string} locales - the locales you want to pull the translations
   * @param {object} params - the parameters to pull the translated document from the Zanata
   * @param {string} [params.potdir='./po'] - the place where store the cache file for source document
   * @param {string} [params.podir='./po'] - the place where store the cache file for translated document
   * @param {number} [params.verbose=0] - Show more progress messages verbosely.
   * @param {boolean} [params.force=false] - Transfer the document without the cache
   * @param {boolean} [params.skeletons=false] - Create skeleton entries even if the document isn't translated.
   *
   * @fires module:zanata/project~Project#fail
   * @fires module:zanata/project~Project#warning
   * @fires module:zanata/project~Project#data_pull
   * @fires module:zanata/project~Project#end_pull
   */
  pullTranslation(projectId, versionId, docId, locales, params) {
    let self = this;
    let result = [];
    let potdir = params && params.potdir || './po';
    let podir = params && params.podir || './po';
    let setag = new ETag(path.join(potdir, 'etag-cache.json'));
    let tetag = potdir === podir ? setag : new ETag(path.join(podir, 'etag-cache.json'));

    if (projectId == undefined)
      projectId = self.config.get('project');
    if (versionId == undefined)
      versionId = self.config.get('project-version');
    if (typeof locales === 'string')
      locales = [locales];

    this.__pullSourceCb(projectId, versionId, docId, setag, params, (e, d) => {
      if (e) {
        self.emit('fail', e);
      } else {
        let fLocales;

        if (locales) {
          fLocales = () => {
            return Promise.resolve(locales);
          };
        } else {
          fLocales = () => {
            let pil = new pilr(self.config.get('config.url'));
            return pil
              .setAuthUser(self.config.get('username'))
              .setAuthToken(self.config.get('api-key'))
              .get(projectId, versionId)
              .then((dd) => {
                let resp = dd.response;
                let data = dd.json || dd.data;

                if (resp.statusCode === 404)
                  throw new Error('No such projects or versions available on the server: [project: ' + projectId + ', version; ' + versionId + ']');
                else if (resp.statusCode !== 200)
                  throw new Error(errmsg(pil.httpStatusMessage(resp, data)));

                return data.map((o) => o.localeId);
              });
          };
        }
        let f = (vv) => {
          return new Promise((resolve, reject) => {
            self._pullTranslationCb(projectId, versionId, docId, d, vv, tetag, params, (ee, dd) => {
              if (ee)
                reject(ee);
              else {
                if (dd)
                  self.emit('data_pull', dd);
                resolve({name: docId, type: 'po', locale: vv, pulled: dd != undefined});
              }
            });
          });
        };
        fLocales()
          .then((v) => {
            return co(function* () {
              let result = [];
              for (let i = 0; i < v.length; i++) {
                result.push(yield f(v[i]));
              }
              return result;
            });
          })
          .then((v) => self.emit('end_pull', v))
          .catch((e) => self.emit('fail', e));
      }
    });
    return self;
  }

  /**
   * Pull the translated documents from the Zanata
   *
   * @param {string} projectId - the project id
   * @param {string} versionId - the project version id
   * @param {string[]|string} locales - the locales you want to pull the translations
   * @param {object} params - the parameters to pull the translated document from the Zanata
   * @param {string} [params.potdir='./po'] - the place where store the cache file for source document
   * @param {string} [params.podir='./po'] - the place where store the cache file for translated document
   * @param {number} [params.verbose=0] - Show more progress messages verbosely.
   * @param {boolean} [params.force=false] - Transfer the document without the cache
   * @param {boolean} [params.skeletons=false] - Create skeleton entries even if the document isn't translated.
   *
   * @fires module:zanata/project~Project#fail
   * @fires module:zanata/project~Project#warning
   * @fires module:zanata/project~Project#data_pull
   * @fires module:zanata/project~Project#end_pull
   */
  pullTranslations(projectId, versionId, locales, params) {
    let self = this;
    let sd = new sdr(self.config.get('config.url'));
    let result = [];
    let fLocales;
    let potdir = params && params.potdir || './po';
    let podir = params && params.podir || './po';
    let setag = new ETag(path.join(potdir, 'etag-cache.json'));
    let tetag = potdir === podir ? setag : new ETag(path.join(podir, 'etag-cache.json'));

    if (projectId == undefined)
      projectId = self.config.get('project');
    if (versionId == undefined)
      versionId = self.config.get('project-version');
    if (typeof locales === 'string')
      locales = [locales];

    if (locales) {
      fLocales = () => Promise.resolve(locales);
    } else {
      fLocales = () => {
        let pil = new pilr(self.config.get('config.url'));
        return pil
          .setAuthUser(self.config.get('username'))
          .setAuthToken(self.config.get('api-key'))
          .get(projectId, versionId)
          .then((dd) => {
            let resp = dd.response;
            let data = dd.json || dd.data;

            if (resp.statusCode === 404)
              throw new Error('No such projects or versions available on the server: [project: ' + projectId + ', version: ' + versionId + ']');
            else if (resp.statusCode !== 200)
              throw new Error(errmsg(pil.httpStatusMessage(resp, data)));

            return data.map((o) => o.localeId);
          });
      };
    }
    let pLocales = fLocales();
    sd
      .setAuthUser(self.config.get('username'))
      .setAuthToken(self.config.get('api-key'))
      .get(projectId, versionId)
      .then((d) => {
        let resp = d.response;
        let data = d.json || d.data;

        if (resp.statusCode === 404)
          throw new Error('No such projects or versions available: [project: ' + projectId + ', version: ' + versionId + ']');
        else if (resp.statusCode !== 200)
          throw new Error(errmsg(sd.httpStatusMessage(resp, data)));

        // obtains sources first
        let fps = (doc) => {
          return new Promise((resolve, reject) => {
            self.__pullSourceCb(projectId, versionId, doc, setag, params, (ee, dd) => {
              if (ee)
                reject(ee);
              else
                resolve(dd);
            });
          });
        };
        return co(function* () {
          let result = [];
          for (let i = 0; i < data.length; i++) {
            yield fps(data[i].name)
              .then((dd) => {
                result.push({name: data[i].name, data: dd});
              })
              .catch((ee) => Promise.reject(ee));
          }
          return result;
        });
      })
      .then((d) => {
        // pulling translations
        let fpt = (doc, body, loc) => {
          return new Promise((resolve, reject) => {
            self._pullTranslationCb(projectId, versionId, doc, body, loc, tetag, params, (ee, dd) => {
              if (ee)
                reject(ee);
              else {
                if (dd)
                  self.emit('data_pull', dd);
                resolve({name: doc, type: 'po', locale: loc, pulled: dd != undefined});
              }
            });
          });
        };
        return fLocales()
          .then((locs) => {
            return co(function* () {
              let result = [];
              for (let i = 0; i < d.length; i++) { // sources
                for (let j = 0; j < locs.length; j++) { // locales
                  yield fpt(d[i].name, d[i].data, locs[j])
                    .then((dd) => result.push(dd))
                    .catch((ee) => {
                      throw ee;
                    });
                }
              }
              return result;
            });
          });
      })
      .then((result) => self.emit('end_pull', result))
      .catch((e) => self.emit('fail', e));

    return self;
  }

  /**
   * Pull the documents from the Zanata
   *
   * @param {object} params - the parameters to pull the documents from the Zanata
   * @param {string} [params.project] - the project id
   * @param {string} [params.version] - the project version id
   * @param {string} [params.pullType='both'] - what the type of document you want to pull. 'source' for the source document, 'trans' for the translated document and 'both'.
   * @param {string|string[]} [params.locales] - the locale names you want to pull. this only takes effect when pullType is 'both' or 'trans'.
   * @param {string} [params.potdir='./po'] - the place where store the cache file for source document
   * @param {string} [params.podir='./po'] - the place where store the cache file for translated document
   * @param {number} [params.verbose=0] - Show more progress messages verbosely.
   * @param {boolean} [params.force=false] - Transfer the document without the cache
   * @param {boolean} [params.skeletons=false] - Create skeleton entries even if the document isn't translated.
   *
   * @fires module:zanata/project~Project#fail
   * @fires module:zanata/project~Project#warning
   * @fires module:zanata/project~Project#data_pull
   * @fires module:zanata/project~Project#end_pull
   */
  pull(params) {
    let self = this;

    if (!params || !params.pullType || params.pullType.toLowerCase() === 'both') {
      let ps = new Project(self.params);
      let pt = new Project(self.params);

      // connect on events
      [ps, pt].forEach((v) => {
        v
          .on('warning', (e) => self.emit('warning', e))
          .on('data_pull', (d) => self.emit('data_pull', d));
      });
      co(function* () {
        let rs = yield new Promise((resolve, reject) => {
          ps
            .on('fail', (e) => reject(e))
            .on('end_pull', (r) => resolve(r))
            .pullSources(params.project, params.version, params);
        });
        let rt = yield new Promise((resolve, reject) => {
          pt
            .on('fail', (e) => reject(e))
            .on('end_pull', (r) => resolve(r))
            .pullTranslations(params.project, params.version, params.locales, params);
        });
        return [rs, rt];
      })
        .then((v) => self.emit('end_pull', _.flatten(v)))
        .catch((e) => self.emit('fail', e));
    } else if (params.pullType.toLowerCase() === 'source') {
      self.pullSources(params.project, params.version, params);
    } else if (params.pullType.toLowerCase() === 'trans') {
      self.pullTranslations(params.project, params.version, params.locales, params);
    } else {
      self.emit('fail', new Error('Unknown pull type: ' + params.pullType));
    }
    return self;
  }

  _pushSourceCb(projectId, versionId, file, params, callback) {
    let self = this;
    let sd = new sdr(self.config.get('config.url'));
    if (projectId == undefined)
      projectId = self.config.get('project');
    if (versionId == undefined)
      versionId = self.config.get('project-version');
    let verbose = params && params.verbose || false;
    let contents;

    try {
      contents = fs.readFileSync(file, 'UTF-8');
    } catch(e) {
      callback(e);
      return self;
    }
    
    if (contents == undefined || contents.length === 0) {
      callback(new Error('No contents in ' + file));
    } else {
      let name = path.basename(file, '.pot');
      Gettext.po2json(name, 'pot', contents)
        .then((data) => {
          let label;

          if (verbose)
            label = util.format('Uploading %s', file);
          if (label)
            console.time(label);
          return sd
            .setAuthUser(self.config.get('username'))
            .setAuthToken(self.config.get('api-key'))
            .putDoc(projectId, versionId, name, data)
            .then((d) => {
              let resp = d.response;
              let data = d.json || d.data;

              if (resp.statusCode === 404)
                throw new Error('No such projects or versions available on the server: [project: ' + projectId + ', version: ' + versionId + ']');
              else if (resp.statusCode === 403)
                throw new Error('Project or Version might be in Read-Only mode: [project: ' + projectId + ', version: ' + versionId + ']');
              else if (resp.statusCode === 401)
                throw new Error('Unauthorized operation');
              else if (resp.statusCode !== 200 && resp.statusCode !== 201)
                throw new Error(sd.httpStatusMessage(resp, data));

              if (label)
                console.timeEnd(label);
              callback(null, file);
            });
        })
        .catch((e) => callback(e));
    }
    return self;
  }

  /**
   * Push the source document to the Zanata
   *
   * @param {string} projectId - the project id
   * @param {string} versionId - the project version id
   * @param {string} file - the filename which you want to push as the source document
   * @param {object} [params] - the parameters to push the source document
   * @param {number} [params.verbose=0] - Show more progress messages verbosely.
   *
   * @fires module:zanata/project~Project#fail
   * @fires module:zanata/project~Project#data_push
   * @fires module:zanata/project~Project#end_push
   */
  pushSource(projectId, versionId, file, params) {
    let self = this;
    self._pushSourceCb(projectId, versionId, file, params, (e, d) => {
      if (e) {
        self.emit('fail', e);
      } else {
        self.emit('data_push', d);
        // for compatibility to other APIs
        self.emit('end_push', [{docId: path.basename(file, '.pot'), type: 'pot', file: file}]);
      }
    });
    return self;
  }

  /**
   * data event from push methods. this event is emitted after pushing data to the Zanata.
   * the passed data contains the filename that pushed to the Zanata.
   *
   * @event module:zanata/project~Project#data_push
   * @type {string}
   */

  /**
   * end event from push methods. this event is emitted when all of pushing is finished.
   *
   * @event module:zanata/project~Project#end_push
   * @type {object[]}
   * @property {string} docId - the document id
   * @property {string} type - the document type pushed. 'pot' for the source document and 'po' for the translated document
   * @property {string} file - the finename pushed to the Zanata.
   * @property {string} [locale] - the locale name pushed to the Zanata. this property is only available when pushing the translated document
   */

  /**
   * Push the source documents to the Zanata
   *
   * @param {string} projectId - the project id
   * @param {string} versionId - the project version id
   * @param {object} [params] - the parameters to push the source document
   * @param {number} [params.verbose=0] - Show more progress messages verbosely.
   * @param {string} [params.potdir='./po'] - the place where read POT files
   * @param {boolean} [params.dryrun=false] - Do not send the data to the Zanata if true.
   *
   * @fires module:zanata/project~Project#fail
   * @fires module:zanata/project~Project#data_push
   * @fires module:zanata/project~Project#end_push
   */
  pushSources(projectId, versionId, params) {
    let self = this;
    if (projectId == undefined)
      projectId = self.config.get('project');
    if (versionId == undefined)
      versionId = self.config.get('project-version');
    let verbose = params && params.verbose || false;
    let potdir = params && params.potdir || './po';
    let dryrun = params && params.dryrun || false;
    let gm = new Glob(path.join(potdir, '**', '*.pot'), {nodir: true, dot: false})
        .on('error', (err) => {
          gm.abort();
          self.emit('fail', err);
        })
        .on('end', (matches) => {
          let f;

          if (dryrun) {
            f = (v) => {
              let vv = path.basename(v, '.pot');
              return Promise.resolve({docId: vv, type: 'pot', file: v});
            };
          } else {
            f = (v) => {
              let vv = path.basename(v, '.pot');
              return new Promise((resolve, reject) => {
                self._pushSourceCb(projectId, versionId, v, params, (e, d) => {
                  if (e)
                    reject(e);
                  else {
                    self.emit('data_push', d);
                    resolve({docId: vv, type: 'pot', file: v});
                  }
                });
              });
            };
          }
          co(function* () {
            let result = [];
            for (let i = 0; i < matches.length; i++) {
              result.push(yield f(matches[i]));
            }
            return result;
          })
            .then((v) => self.emit('end_push', v))
            .catch((e) => self.emit('fail', e));
        });
  }

  _pushTranslationCb(projectId, versionId, docId, locale, file, params, callback) {
    let self = this;
    if (projectId == undefined)
      projectId = self.config.get('project');
    if (versionId == undefined)
      versionId = self.config.get('project-version');
    let verbose = params && params.verbose || false;
    let copyTrans = params && params.copyTrans || false;
    let contents;

    try {
      contents = fs.readFileSync(file, 'UTF-8');
    } catch(e) {
      callback(e);
      return self;
    }
    if (contents == undefined || contents.length === 0) {
      callback(new Error('No contents in ' + file));
    } else {
      Gettext.po2json(docId, 'po', contents)
        .then((data) => {
          let label;
          let td = new tdr(self.config.get('config.url'));

          if (verbose)
            label = 'Uploading ' + file;
          if (label)
            console.time(label);
          return td
            .setAuthUser(self.config.get('username'))
            .setAuthToken(self.config.get('api-key'))
            .put(projectId, versionId, docId, locale, data, copyTrans)
            .then((d) => {
              let resp = d.response;
              let data = d.json || d.data;

              if (resp.statusCode === 404)
                throw new Error('No such projects, versions, or documents available on the server: [project: ' + projectId + ', version: ' + versionId + ', doc: ' + docId + ']');
              else if (resp.statusCode === 401)
                throw new Error('Unauthorized operation');
              else if (resp.statusCode !== 200)
                throw new Error(errmsg(td.httpStatusMessage(resp, data)));

              if (label)
                console.timeEnd(label);
              callback(null, file);
            });
        })
        .catch((e) => callback(e));
    }
    return self;
  }

  /**
   * Push the translated document to the Zanata
   *
   * @param {string} projectId - the project id
   * @param {string} versionId - the project version id
   * @param {string} docId - the document id
   * @param {string} locale - the locale name pushing the document
   * @param {string} file - the filename which you want to push as the translated document
   * @param {object} [params] - the parameters to push the translated document
   * @param {number} [params.verbose=0] - Show more progress messages verbosely.
   * @param {boolean} [params.copyTrans=false] - Copy the latest translations from equivalent messages/documents in the Zanata if true
   *
   * @fires module:zanata/project~Project#fail
   * @fires module:zanata/project~Project#data_push
   * @fires module:zanata/project~Project#end_push
   */
  pushTranslation(projectId, versionId, docId, locale, file, params) {
    let self = this;
  
    if (projectId == undefined)
      projectId = self.config.get('project');
    if (versionId == undefined)
      versionId = self.config.get('project-version');
    if (locale instanceof Array) {
      self.emit('fail', new Error('Only single locale is supported. Use pushTranslations() instead.'));
      return self;
    }

    self._pushTranslationCb(projectId, versionId, docId, locale, file, params, (e, d) => {
      if (e) {
        self.emit('fail', e);
      } else {
        self.emit('data_push', d);
        self.emit('end_push', {
          docId: docId,
          locale: locale,
          type: 'po',
          file: file
        });
      }
    });
    return self;
  }

  /**
   * Push the translated documents to the Zanata
   *
   * @param {string} projectId - the project id
   * @param {string} versionId - the project version id
   * @param {string[]} locales - the locale name pushing the documents
   * @param {object} [params] - the parameters to push the translated documents
   * @param {string} [params.project-type] - the project type
   * @param {string} [params.podir='./po'] - the place where read PO files
   * @param {string} [params.potdir='./po'] - the place for POT files
   * @param {number} [params.verbose=0] - Show more progress messages verbosely.
   * @param {boolean} [params.copyTrans=false] - Copy the latest translations from equivalent messages/documents in the Zanata if true
   * @param {boolean} [params.dryrun=false] - Do not send the data to the Zanata if true.
   *
   * @fires module:zanata/project~Project#fail
   * @fires module:zanata/project~Project#warning
   * @fires module:zanata/project~Project#data_push
   * @fires module:zanata/project~Project#end_push
   */
  pushTranslations(projectId, versionId, locales, params) {
    let self = this;
    let sd = new sdr(self.config.get('config.url'));
    if (projectId == undefined)
      projectId = self.config.get('project');
    if (versionId == undefined)
      versionId = self.config.get('project-version');
    let projectType = params && params.projectType || self.config.get('project-type');
    let verbose = params && params.verbose || false;
    let copyTrans = params && params.copyTrans || false;
    let potdir = params && params.potdir || './po';
    let podir = params && params.podir || './po';
    let dryrun = params && params.dryrun || false;
    let fLocales;

    if (locales)
      fLocales = () => Promise.resolve(locales);
    else {
      fLocales = () => {
        let pil = new pilr(self.config.get('config.url'));
        return pil
          .setAuthUser(self.config.get('username'))
          .setAuthToken(self.config.get('api-key'))
          .get(projectId, versionId)
          .then((dd) => {
            let resp = dd.response;
            let data = dd.json || dd.data;

            if (resp.statusCode === 404)
              throw new Error('No such projects or versions available on the server: [project: ' + projectId + ', version: ' + versionId + ']');
            else if (resp.statusCode !== 200)
              throw new Error(errmsg(pil.httpStatusMessage(resp, data)));

            return data.map((o) => o.localeId);
          });
      };
    }
    sd
      .setAuthUser(self.config.get('username'))
      .setAuthToken(self.config.get('api-key'))
      .get(projectId, versionId)
      .then((d) => {
        let resp = d.response;
        let data = d.json || d.data;

        if (resp.statusCode === 404)
          throw new Error('No such projects or versions available on the server: [project: ' + projectId + ', version: ' + versionId + ']');
        else if (resp.statusCode !== 200)
          throw new Error(errmsg(sd.httpStatusMessage(resp, data)));

        let fpt = (doc, loc, file) => {
          return new Promise((resolve, reject) => {
            self._pushTranslationCb(projectId, versionId, doc, loc, file, params, (ee, dd) => {
              if (ee)
                reject(ee);
              else {
                self.emit('data_push', dd);
                resolve({docId: doc, locale:  loc, type: 'po', file: file});
              }
            });
          });
        };
        return fLocales()
          .then((locs) => {
            let fm = new fmr(self.config.get('rules'));

            return co(function* () {
              let result = [];
              for (let i = 0; i < data.length; i++) { // docs
                for (let j = 0; j < locs.length; j++) { // locales
                  let file = fm.getRealPath(projectType, {path: podir, locale: locs[j], filenamem: data[i].name, extension: 'po', source: path.join(potdir, data[i].name + '.pot')});

                  if (file) {
                    yield fpt(data[i].name, locs[j], file)
                      .then((dd) => result.push(d))
                      .catch((ee) => {
                        throw ee;
                      });
                  } else {
                    self.emit('warning', 'Unable to determine the file path: [doc: ' + data[i].name + ', locale: ' + locs[j] + ']');
                  }
                }
              }
              return result;
            });
          });
      })
      .then((result) => self.emit('end_push', result))
      .catch((e) => self.emit('fail', e));

    return self;
  }

  /**
   * Push the documents to the Zanata
   *
   * @param {object} [params] - the parameters to push the documents
   * @param {string} [params.pushType='both'] - what the type of the document you want to push. 'source' for the source documents, 'trans' for the translated documents, and 'both'.
   * @param {string} [params.project] - the project id
   * @param {string} [params.version] - the project version id
   * @param {string} [params.locales] - the locale names you want to push
   * @param {string} [params.project-type] - the project type
   * @param {string} [params.potdir='./po'] - the place where read POT files
   * @param {string} [params.podir='./po'] - the place where read PO files
   * @param {number} [params.verbose=0] - Show more progress messages verbosely.
   * @param {boolean} [params.copyTrans=false] - Copy the latest translations from equivalent messages/documents in the Zanata if true
   * @param {boolean} [params.dryrun=false] - Do not send the data to the Zanata if true.
   *
   * @fires module:zanata/project~Project#fail
   * @fires module:zanata/project~Project#warning
   * @fires module:zanata/project~Project#data_push
   * @fires module:zanata/project~Project#end_push
   */
  push(params) {
    let self = this;
    let fdryrun, frun;

    if (!params || !params.pushType || params.pushType.toLowerCase() === 'both') {
      let ps = new Project(self.params);
      let pt = new Project(self.params);

      // connect on events
      [ps, pt].forEach((v) => {
        v
          .on('warning', (e) => self.emit('warning', e))
          .on('data_push', (d) => self.emit('data_push', d));
      });
      co(function* () {
        let rs = yield new Promise((resolve, reject) => {
          ps
            .on('fail', (e) => reject(e))
            .on('end_push', (r) => resolve(r))
            .pushSources(params.project, params.version, params);
        });
        let rt = yield new Promise((resolve, reject) => {
          pt
            .on('fail', (e) => reject(e))
            .on('end_push', (r) => resolve(r))
            .pushTranslations(params.project, params.version, params.locales, params);
        });
        return [rs, rt];
      })
        .then((v) => self.emit('end_push', _.flatten(v)))
        .catch((e) => self.emit('fail', e));
    } else if (params.pushType.toLowerCase() === 'source') {
      self.pushSources(params.project, params.version, params);
    } else if (params.pushType.toLowerCase() === 'trans') {
      self.pushTranslations(params.project, params.version, params.locales, params);
    } else {
      self.emit('fail', new Error('Unknown push type: ' + params.pushType));
    }
    return self;
  }

  /**
   * data event from stats method
   *
   * @event module:zanata/project~Project#data_stats
   * @type {object}
   * @property {string} id
   * @property {object[]} stats
   * @property {number} stats.total
   * @property {number} stats.untranslated
   * @property {number} stats.needReview
   * @property {number} stats.translated
   * @property {number} stats.approved
   * @property {number} stats.rejected
   * @property {number} stats.fuzzy
   * @property {string} stats.unit
   * @property {string} stats.locale
   * @property {string} stats.lastTranslated
   * @property {number} stats.translatedOnly
   * @property {object[]} detailedStats
   *
   * @see {@link https://zanata.ci.cloudbees.com/job/zanata-api-site/site/zanata-common-api/rest-api-docs/json_TranslationStatistics.html|TranslationStatistics data type}
   */

  /**
   * Obtain document statistics from the Zanata
   *
   * @param {string} projectId - the project id
   * @param {string} versionId - the project version id
   * @param {string} [docId] - the document id
   * @param {object} [params] - the parameters to obtain the document statistics
   * @param {string[]|string} [params.locales] - the locale names you want to obtain the statistics
   * @param {boolean} [params.detail=false] - whether include the detailed statistics
   * @param {boolean} [params.word=false] - whether include the word-level statistics
   *
   * @fires module:zanata/project~Project#fail
   * @fires module:zanata/project~Project#data_stats
   */
  stats() {
    let args = arguments;
    let self = this;
    // assume the first function argument should be a callback
    let projectId = args[0] || self.config.get('project');
    let versionId = args[1] || self.config.get('project-version');
    let docId, username, dateRange, params;
    let stat = new sr(self.config.get('config.url'))
        .setAuthUser(self.config.get('username'))
        .setAuthToken(self.config.get('api-key'));

    if (args.length === 3) {
      // Get translation statistics for a Project iteration and (optionally) it's underlying documents.
      // projectId, versionId, params
      params = args[2];
      stat.get(projectId, versionId, params)
        .then((d) => {
          let resp = d.response;
          let data = d.json || d.data;

          if (resp.statuscode === 404)
            throw new Error('No such projects or versions available on the server: [project: ' + projectId + ', version: ' + versionId + ']');
          else if (resp.statusCode !== 200)
            throw new Error(errmsg(stat.httpStatusMessage(resp, data)));

          self.emit('data_stats', data);
        })
        .catch((e) => self.emit('fail', e));
    } else if (args.length === 4) {
      // Get translation statistics for a Document
      // projectId, versionId, docId, params
      docId = args[2];
      params = args[3];
      stat.getDoc(projectId, versionId, docId, params)
        .then((d) => {
          let resp = d.response;
          let data = d.json || d.data;

          if (resp.statusCode === 404)
            throw new Error('No such projects, versions, or documents available on the server: [project: ' + projectId + ', version: ' + versionId + ', doc: ' + docId + ']');
          else if (resp.statusCode !== 200)
            throw new Error(errmsg(stat.httpStatusMessage(resp, data)));

          self.emit('data_stats', data);
        })
        .catch((e) => self.emit('fail', e));
    } else if (args.length === 5) {
      // Get contribution statistic from project-version within given date range.
      // projectId, versionId, username, dateRange, params
      username = args[2];
      dateRange = args[3];
      params = args[4];
      self.emit('fail', new Error('not yet supported'));
    } else {
      self.emit('fail', new Error('Invalid parameters'));
    }

    return this;
  }

}

exports.Project = Project;