text: "use strict";
  
  var get    = require("lodash.get");
  var Errors = require("./Errors");
  var Utils  = require("./Utils");
  
  /**
   * @class
   *
   * @param {Report|object} parentOrOptions
   * @param {object} [reportOptions]
   */
  function Report(parentOrOptions, reportOptions) {
      this.parentReport = parentOrOptions instanceof Report ?
                              parentOrOptions :
                              undefined;
  
      this.options = parentOrOptions instanceof Report ?
                         parentOrOptions.options :
                         parentOrOptions || {};
  
      this.reportOptions = reportOptions || {};
  
      this.errors = [];
      /**
       * @type {string[]}
       */
      this.path = [];
      this.asyncTasks = [];
  
      this.rootSchema = undefined;
      this.commonErrorMessage = undefined;
      this.json = undefined;
  }
  
  /**
   * @returns {boolean}
   */
  Report.prototype.isValid = function () {
      if (this.asyncTasks.length > 0) {
          throw new Error("Async tasks pending, can't answer isValid");
      }
      return this.errors.length === 0;
  };
  
  /**
   *
   * @param {*} fn
   * @param {*} args
   * @param {*} asyncTaskResultProcessFn
   */
  Report.prototype.addAsyncTask = function (fn, args, asyncTaskResultProcessFn) {
      this.asyncTasks.push([fn, args, asyncTaskResultProcessFn]);
  };
  
  /**
   *
   * @param {*} timeout
   * @param {function(*, *)} callback
   *
   * @returns {void}
   */
  Report.prototype.processAsyncTasks = function (timeout, callback) {
  
      var validationTimeout = timeout || 2000,
          tasksCount        = this.asyncTasks.length,
          idx               = tasksCount,
          timedOut          = false,
          self              = this;
  
      function finish() {
          process.nextTick(function () {
              var valid = self.errors.length === 0,
                  err = valid ? undefined : self.errors;
              callback(err, valid);
          });
      }
  
      function respond(asyncTaskResultProcessFn) {
          return function (asyncTaskResult) {
              if (timedOut) { return; }
              asyncTaskResultProcessFn(asyncTaskResult);
              if (--tasksCount === 0) {
                  finish();
              }
          };
      }
  
      // finish if tasks are completed or there are any errors and breaking on first error was requested
      if (tasksCount === 0 || (this.errors.length > 0 && this.options.breakOnFirstError)) {
          finish();
          return;
      }
  
      while (idx--) {
          var task = this.asyncTasks[idx];
          task[0].apply(null, task[1].concat(respond(task[2])));
      }
  
      setTimeout(function () {
          if (tasksCount > 0) {
              timedOut = true;
              self.addError("ASYNC_TIMEOUT", [tasksCount, validationTimeout]);
              callback(self.errors, false);
          }
      }, validationTimeout);
  
  };
  
  /**
   *
   * @param {*} returnPathAsString
   *
   * @return {string[]|string}
   */
  Report.prototype.getPath = function (returnPathAsString) {
      /**
       * @type {string[]|string}
       */
      var path = [];
      if (this.parentReport) {
          path = path.concat(this.parentReport.path);
      }
      path = path.concat(this.path);
  
      if (returnPathAsString !== true) {
          // Sanitize the path segments (http://tools.ietf.org/html/rfc6901#section-4)
          path = "#/" + path.map(function (segment) {
  
              if (Utils.isAbsoluteUri(segment)) {
                  return "uri(" + segment + ")";
              }
  
              return segment.replace(/\~/g, "~0").replace(/\//g, "~1");
          }).join("/");
      }
      return path;
  };
  
  Report.prototype.getSchemaId = function () {
  
      if (!this.rootSchema) {
          return null;
      }
  
      // get the error path as an array
      var path = [];
      if (this.parentReport) {
          path = path.concat(this.parentReport.path);
      }
      path = path.concat(this.path);
  
      // try to find id in the error path
      while (path.length > 0) {
          var obj = get(this.rootSchema, path);
          if (obj && obj.id) { return obj.id; }
          path.pop();
      }
  
      // return id of the root
      return this.rootSchema.id;
  };
  
  /**
   *
   * @param {*} errorCode
   * @param {*} params
   *
   * @return {boolean}
   */
  Report.prototype.hasError = function (errorCode, params) {
      var idx = this.errors.length;
      while (idx--) {
          if (this.errors[idx].code === errorCode) {
              // assume match
              var match = true;
  
              // check the params too
              var idx2 = this.errors[idx].params.length;
              while (idx2--) {
                  if (this.errors[idx].params[idx2] !== params[idx2]) {
                      match = false;
                  }
              }
  
              // if match, return true
              if (match) { return match; }
          }
      }
      return false;
  };
  
  /**
   *
   * @param {*} errorCode
   * @param {*} params
   * @param {Report[]|Report} [subReports]
   * @param {*} [schema]
   *
   * @return {void}
   */
  Report.prototype.addError = function (errorCode, params, subReports, schema) {
      if (!errorCode) { throw new Error("No errorCode passed into addError()"); }
  
      this.addCustomError(errorCode, Errors[errorCode], params, subReports, schema);
  };
  
  Report.prototype.getJson = function () {
      var self = this;
      while (self.json === undefined) {
          self = self.parentReport;
          if (self === undefined) {
              return undefined;
          }
      }
      return self.json;
  };
  
  /**
   *
   * @param {*} errorCode
   * @param {*} errorMessage
   * @param {*[]} params
   * @param {Report[]|Report} subReports
   * @param {*} schema
   *
   * @returns {void}
   */
  Report.prototype.addCustomError = function (errorCode, errorMessage, params, subReports, schema) {
      if (this.errors.length >= this.reportOptions.maxErrors) {
          return;
      }
  
      if (!errorMessage) { throw new Error("No errorMessage known for code " + errorCode); }
  
      params = params || [];
  
      var idx = params.length;
      while (idx--) {
          var whatIs = Utils.whatIs(params[idx]);
          var param = (whatIs === "object" || whatIs === "null") ? JSON.stringify(params[idx]) : params[idx];
          errorMessage = errorMessage.replace("{" + idx + "}", param);
      }
  
      var err = {
          code: errorCode,
          params: params,
          message: errorMessage,
          path: this.getPath(this.options.reportPathAsArray),
          schemaId: this.getSchemaId()
      };
  
      err[Utils.schemaSymbol] = schema;
      err[Utils.jsonSymbol] = this.getJson();
  
      if (schema && typeof schema === "string") {
          err.description = schema;
      } else if (schema && typeof schema === "object") {
          if (schema.title) {
              err.title = schema.title;
          }
          if (schema.description) {
              err.description = schema.description;
          }
      }
  
      if (subReports != null) {
          if (!Array.isArray(subReports)) {
              subReports = [subReports];
          }
          err.inner = [];
          idx = subReports.length;
          while (idx--) {
              var subReport = subReports[idx],
                  idx2 = subReport.errors.length;
              while (idx2--) {
                  err.inner.push(subReport.errors[idx2]);
              }
          }
          if (err.inner.length === 0) {
              err.inner = undefined;
          }
      }
  
      this.errors.push(err);
  };
  
  module.exports = Report;
  