text: "use strict";
  module.exports = Namespace;
  
  // extends ReflectionObject
  var ReflectionObject = require("./object");
  ((Namespace.prototype = Object.create(ReflectionObject.prototype)).constructor = Namespace).className = "Namespace";
  
  var Field    = require("./field"),
      util     = require("./util");
  
  var Type,    // cyclic
      Service,
      Enum;
  
  /**
   * Constructs a new namespace instance.
   * @name Namespace
   * @classdesc Reflected namespace.
   * @extends NamespaceBase
   * @constructor
   * @param {string} name Namespace name
   * @param {Object.<string,*>} [options] Declared options
   */
  
  /**
   * Constructs a namespace from JSON.
   * @memberof Namespace
   * @function
   * @param {string} name Namespace name
   * @param {Object.<string,*>} json JSON object
   * @returns {Namespace} Created namespace
   * @throws {TypeError} If arguments are invalid
   */
  Namespace.fromJSON = function fromJSON(name, json) {
      return new Namespace(name, json.options).addJSON(json.nested);
  };
  
  /**
   * Converts an array of reflection objects to JSON.
   * @memberof Namespace
   * @param {ReflectionObject[]} array Object array
   * @param {IToJSONOptions} [toJSONOptions] JSON conversion options
   * @returns {Object.<string,*>|undefined} JSON object or `undefined` when array is empty
   */
  function arrayToJSON(array, toJSONOptions) {
      if (!(array && array.length))
          return undefined;
      var obj = {};
      for (var i = 0; i < array.length; ++i)
          obj[array[i].name] = array[i].toJSON(toJSONOptions);
      return obj;
  }
  
  Namespace.arrayToJSON = arrayToJSON;
  
  /**
   * Tests if the specified id is reserved.
   * @param {Array.<number[]|string>|undefined} reserved Array of reserved ranges and names
   * @param {number} id Id to test
   * @returns {boolean} `true` if reserved, otherwise `false`
   */
  Namespace.isReservedId = function isReservedId(reserved, id) {
      if (reserved)
          for (var i = 0; i < reserved.length; ++i)
              if (typeof reserved[i] !== "string" && reserved[i][0] <= id && reserved[i][1] > id)
                  return true;
      return false;
  };
  
  /**
   * Tests if the specified name is reserved.
   * @param {Array.<number[]|string>|undefined} reserved Array of reserved ranges and names
   * @param {string} name Name to test
   * @returns {boolean} `true` if reserved, otherwise `false`
   */
  Namespace.isReservedName = function isReservedName(reserved, name) {
      if (reserved)
          for (var i = 0; i < reserved.length; ++i)
              if (reserved[i] === name)
                  return true;
      return false;
  };
  
  /**
   * Not an actual constructor. Use {@link Namespace} instead.
   * @classdesc Base class of all reflection objects containing nested objects. This is not an actual class but here for the sake of having consistent type definitions.
   * @exports NamespaceBase
   * @extends ReflectionObject
   * @abstract
   * @constructor
   * @param {string} name Namespace name
   * @param {Object.<string,*>} [options] Declared options
   * @see {@link Namespace}
   */
  function Namespace(name, options) {
      ReflectionObject.call(this, name, options);
  
      /**
       * Nested objects by name.
       * @type {Object.<string,ReflectionObject>|undefined}
       */
      this.nested = undefined; // toJSON
  
      /**
       * Cached nested objects as an array.
       * @type {ReflectionObject[]|null}
       * @private
       */
      this._nestedArray = null;
  }
  
  function clearCache(namespace) {
      namespace._nestedArray = null;
      return namespace;
  }
  
  /**
   * Nested objects of this namespace as an array for iteration.
   * @name NamespaceBase#nestedArray
   * @type {ReflectionObject[]}
   * @readonly
   */
  Object.defineProperty(Namespace.prototype, "nestedArray", {
      get: function() {
          return this._nestedArray || (this._nestedArray = util.toArray(this.nested));
      }
  });
  
  /**
   * Namespace descriptor.
   * @interface INamespace
   * @property {Object.<string,*>} [options] Namespace options
   * @property {Object.<string,AnyNestedObject>} [nested] Nested object descriptors
   */
  
  /**
   * Any extension field descriptor.
   * @typedef AnyExtensionField
   * @type {IExtensionField|IExtensionMapField}
   */
  
  /**
   * Any nested object descriptor.
   * @typedef AnyNestedObject
   * @type {IEnum|IType|IService|AnyExtensionField|INamespace}
   */
  // ^ BEWARE: VSCode hangs forever when using more than 5 types (that's why AnyExtensionField exists in the first place)
  
  /**
   * Converts this namespace to a namespace descriptor.
   * @param {IToJSONOptions} [toJSONOptions] JSON conversion options
   * @returns {INamespace} Namespace descriptor
   */
  Namespace.prototype.toJSON = function toJSON(toJSONOptions) {
      return util.toObject([
          "options" , this.options,
          "nested"  , arrayToJSON(this.nestedArray, toJSONOptions)
      ]);
  };
  
  /**
   * Adds nested objects to this namespace from nested object descriptors.
   * @param {Object.<string,AnyNestedObject>} nestedJson Any nested object descriptors
   * @returns {Namespace} `this`
   */
  Namespace.prototype.addJSON = function addJSON(nestedJson) {
      var ns = this;
      /* istanbul ignore else */
      if (nestedJson) {
          for (var names = Object.keys(nestedJson), i = 0, nested; i < names.length; ++i) {
              nested = nestedJson[names[i]];
              ns.add( // most to least likely
                  ( nested.fields !== undefined
                  ? Type.fromJSON
                  : nested.values !== undefined
                  ? Enum.fromJSON
                  : nested.methods !== undefined
                  ? Service.fromJSON
                  : nested.id !== undefined
                  ? Field.fromJSON
                  : Namespace.fromJSON )(names[i], nested)
              );
          }
      }
      return this;
  };
  
  /**
   * Gets the nested object of the specified name.
   * @param {string} name Nested object name
   * @returns {ReflectionObject|null} The reflection object or `null` if it doesn't exist
   */
  Namespace.prototype.get = function get(name) {
      return this.nested && this.nested[name]
          || null;
  };
  
  /**
   * Gets the values of the nested {@link Enum|enum} of the specified name.
   * This methods differs from {@link Namespace#get|get} in that it returns an enum's values directly and throws instead of returning `null`.
   * @param {string} name Nested enum name
   * @returns {Object.<string,number>} Enum values
   * @throws {Error} If there is no such enum
   */
  Namespace.prototype.getEnum = function getEnum(name) {
      if (this.nested && this.nested[name] instanceof Enum)
          return this.nested[name].values;
      throw Error("no such enum: " + name);
  };
  
  /**
   * Adds a nested object to this namespace.
   * @param {ReflectionObject} object Nested object to add
   * @returns {Namespace} `this`
   * @throws {TypeError} If arguments are invalid
   * @throws {Error} If there is already a nested object with this name
   */
  Namespace.prototype.add = function add(object) {
  
      if (!(object instanceof Field && object.extend !== undefined || object instanceof Type || object instanceof Enum || object instanceof Service || object instanceof Namespace))
          throw TypeError("object must be a valid nested object");
  
      if (!this.nested)
          this.nested = {};
      else {
          var prev = this.get(object.name);
          if (prev) {
              if (prev instanceof Namespace && object instanceof Namespace && !(prev instanceof Service)) {
                  // replace plain namespace or type, but keep existing nested elements and options
                  var nested = prev.nestedArray;
                  for (var i = 0; i < nested.length; ++i)
                      object.add(nested[i]);
                  this.remove(prev);
                  if (!this.nested)
                      this.nested = {};
                  object.setOptions(prev.options, true);
  
              } else
                  throw Error("duplicate name '" + object.name + "' in " + this);
          }
      }
      this.nested[object.name] = object;
      object.onAdd(this);
      return clearCache(this);
  };
  
  /**
   * Removes a nested object from this namespace.
   * @param {ReflectionObject} object Nested object to remove
   * @returns {Namespace} `this`
   * @throws {TypeError} If arguments are invalid
   * @throws {Error} If `object` is not a member of this namespace
   */
  Namespace.prototype.remove = function remove(object) {
  
      if (!(object instanceof ReflectionObject))
          throw TypeError("object must be a ReflectionObject");
      if (object.parent !== this)
          throw Error(object + " is not a member of " + this);
  
      delete this.nested[object.name];
      if (!Object.keys(this.nested).length)
          this.nested = undefined;
  
      object.onRemove(this);
      return clearCache(this);
  };
  
  /**
   * Defines additial namespaces within this one if not yet existing.
   * @param {string|string[]} path Path to create
   * @param {*} [json] Nested types to create from JSON
   * @returns {Namespace} Pointer to the last namespace created or `this` if path is empty
   */
  Namespace.prototype.define = function define(path, json) {
  
      if (util.isString(path))
          path = path.split(".");
      else if (!Array.isArray(path))
          throw TypeError("illegal path");
      if (path && path.length && path[0] === "")
          throw Error("path must be relative");
  
      var ptr = this;
      while (path.length > 0) {
          var part = path.shift();
          if (ptr.nested && ptr.nested[part]) {
              ptr = ptr.nested[part];
              if (!(ptr instanceof Namespace))
                  throw Error("path conflicts with non-namespace objects");
          } else
              ptr.add(ptr = new Namespace(part));
      }
      if (json)
          ptr.addJSON(json);
      return ptr;
  };
  
  /**
   * Resolves this namespace's and all its nested objects' type references. Useful to validate a reflection tree, but comes at a cost.
   * @returns {Namespace} `this`
   */
  Namespace.prototype.resolveAll = function resolveAll() {
      var nested = this.nestedArray, i = 0;
      while (i < nested.length)
          if (nested[i] instanceof Namespace)
              nested[i++].resolveAll();
          else
              nested[i++].resolve();
      return this.resolve();
  };
  
  /**
   * Recursively looks up the reflection object matching the specified path in the scope of this namespace.
   * @param {string|string[]} path Path to look up
   * @param {*|Array.<*>} filterTypes Filter types, any combination of the constructors of `protobuf.Type`, `protobuf.Enum`, `protobuf.Service` etc.
   * @param {boolean} [parentAlreadyChecked=false] If known, whether the parent has already been checked
   * @returns {ReflectionObject|null} Looked up object or `null` if none could be found
   */
  Namespace.prototype.lookup = function lookup(path, filterTypes, parentAlreadyChecked) {
  
      /* istanbul ignore next */
      if (typeof filterTypes === "boolean") {
          parentAlreadyChecked = filterTypes;
          filterTypes = undefined;
      } else if (filterTypes && !Array.isArray(filterTypes))
          filterTypes = [ filterTypes ];
  
      if (util.isString(path) && path.length) {
          if (path === ".")
              return this.root;
          path = path.split(".");
      } else if (!path.length)
          return this;
  
      // Start at root if path is absolute
      if (path[0] === "")
          return this.root.lookup(path.slice(1), filterTypes);
  
      // Test if the first part matches any nested object, and if so, traverse if path contains more
      var found = this.get(path[0]);
      if (found) {
          if (path.length === 1) {
              if (!filterTypes || filterTypes.indexOf(found.constructor) > -1)
                  return found;
          } else if (found instanceof Namespace && (found = found.lookup(path.slice(1), filterTypes, true)))
              return found;
  
      // Otherwise try each nested namespace
      } else
          for (var i = 0; i < this.nestedArray.length; ++i)
              if (this._nestedArray[i] instanceof Namespace && (found = this._nestedArray[i].lookup(path, filterTypes, true)))
                  return found;
  
      // If there hasn't been a match, try again at the parent
      if (this.parent === null || parentAlreadyChecked)
          return null;
      return this.parent.lookup(path, filterTypes);
  };
  
  /**
   * Looks up the reflection object at the specified path, relative to this namespace.
   * @name NamespaceBase#lookup
   * @function
   * @param {string|string[]} path Path to look up
   * @param {boolean} [parentAlreadyChecked=false] Whether the parent has already been checked
   * @returns {ReflectionObject|null} Looked up object or `null` if none could be found
   * @variation 2
   */
  // lookup(path: string, [parentAlreadyChecked: boolean])
  
  /**
   * Looks up the {@link Type|type} at the specified path, relative to this namespace.
   * Besides its signature, this methods differs from {@link Namespace#lookup|lookup} in that it throws instead of returning `null`.
   * @param {string|string[]} path Path to look up
   * @returns {Type} Looked up type
   * @throws {Error} If `path` does not point to a type
   */
  Namespace.prototype.lookupType = function lookupType(path) {
      var found = this.lookup(path, [ Type ]);
      if (!found)
          throw Error("no such type: " + path);
      return found;
  };
  
  /**
   * Looks up the values of the {@link Enum|enum} at the specified path, relative to this namespace.
   * Besides its signature, this methods differs from {@link Namespace#lookup|lookup} in that it throws instead of returning `null`.
   * @param {string|string[]} path Path to look up
   * @returns {Enum} Looked up enum
   * @throws {Error} If `path` does not point to an enum
   */
  Namespace.prototype.lookupEnum = function lookupEnum(path) {
      var found = this.lookup(path, [ Enum ]);
      if (!found)
          throw Error("no such Enum '" + path + "' in " + this);
      return found;
  };
  
  /**
   * Looks up the {@link Type|type} or {@link Enum|enum} at the specified path, relative to this namespace.
   * Besides its signature, this methods differs from {@link Namespace#lookup|lookup} in that it throws instead of returning `null`.
   * @param {string|string[]} path Path to look up
   * @returns {Type} Looked up type or enum
   * @throws {Error} If `path` does not point to a type or enum
   */
  Namespace.prototype.lookupTypeOrEnum = function lookupTypeOrEnum(path) {
      var found = this.lookup(path, [ Type, Enum ]);
      if (!found)
          throw Error("no such Type or Enum '" + path + "' in " + this);
      return found;
  };
  
  /**
   * Looks up the {@link Service|service} at the specified path, relative to this namespace.
   * Besides its signature, this methods differs from {@link Namespace#lookup|lookup} in that it throws instead of returning `null`.
   * @param {string|string[]} path Path to look up
   * @returns {Service} Looked up service
   * @throws {Error} If `path` does not point to a service
   */
  Namespace.prototype.lookupService = function lookupService(path) {
      var found = this.lookup(path, [ Service ]);
      if (!found)
          throw Error("no such Service '" + path + "' in " + this);
      return found;
  };
  
  // Sets up cyclic dependencies (called in index-light)
  Namespace._configure = function(Type_, Service_, Enum_) {
      Type    = Type_;
      Service = Service_;
      Enum    = Enum_;
  };
  