text: import { EventHandler } from '../core/event-handler.js';
  import { Tags } from '../core/tags.js';
  
  import { Mat3 } from '../math/mat3.js';
  import { Mat4 } from '../math/mat4.js';
  import { Quat } from '../math/quat.js';
  import { Vec3 } from '../math/vec3.js';
  
  const scaleCompensatePosTransform = new Mat4();
  const scaleCompensatePos = new Vec3();
  const scaleCompensateRot = new Quat();
  const scaleCompensateRot2 = new Quat();
  const scaleCompensateScale = new Vec3();
  const scaleCompensateScaleForParent = new Vec3();
  const tmpMat4 = new Mat4();
  const tmpQuat = new Quat();
  const position = new Vec3();
  const invParentWtm = new Mat4();
  const rotation = new Quat();
  const invParentRot = new Quat();
  const matrix = new Mat4();
  const target = new Vec3();
  const up = new Vec3();
  
  /**
   * Callback used by {@link GraphNode#find} and {@link GraphNode#findOne} to search through a graph
   * node and all of its descendants.
   *
   * @callback findNodeCallback
   * @param {GraphNode} node - The current graph node.
   * @returns {boolean} Returning `true` will result in that node being returned from
   * {@link GraphNode#find} or {@link GraphNode#findOne}.
   */
  
  /**
   * Callback used by {@link GraphNode#forEach} to iterate through a graph node and all of its
   * descendants.
   *
   * @callback forEachNodeCallback
   * @param {GraphNode} node - The current graph node.
   */
  
  /**
   * A hierarchical scene node.
   *
   * @property {string} name The non-unique name of a graph node.
   * @property {Tags} tags Interface for tagging graph nodes. Tag based searches can be performed using the {@link GraphNode#findByTag} function.
   * @augments EventHandler
   */
  class GraphNode extends EventHandler {
      /**
       * Create a new GraphNode instance.
       *
       * @param {string} [name] - The non-unique name of the graph node, default is "Untitled".
       */
      constructor(name) {
          super();
  
          this.name = typeof name === "string" ? name : "Untitled"; // Non-unique human readable name
          this.tags = new Tags(this);
  
          this._labels = {};
  
          // Local-space properties of transform (only first 3 are settable by the user)
          this.localPosition = new Vec3(0, 0, 0);
          this.localRotation = new Quat(0, 0, 0, 1);
          this.localScale = new Vec3(1, 1, 1);
          this.localEulerAngles = new Vec3(0, 0, 0); // Only calculated on request
  
          // World-space properties of transform
          this.position = new Vec3(0, 0, 0);
          this.rotation = new Quat(0, 0, 0, 1);
          this.eulerAngles = new Vec3(0, 0, 0);
          this._scale = null;
  
          this.localTransform = new Mat4();
          this._dirtyLocal = false;
          this._aabbVer = 0;
  
          // _frozen flag marks the node to ignore hierarchy sync entirely (including children nodes)
          // engine code automatically freezes and unfreezes objects whenever required
          // segregating dynamic and stationary nodes into subhierarchies allows to reduce sync time significantly
          this._frozen = false;
  
          this.worldTransform = new Mat4();
          this._dirtyWorld = false;
  
          this.normalMatrix = new Mat3();
          this._dirtyNormal = true;
  
          this._right = null;
          this._up = null;
          this._forward = null;
  
          this._parent = null;
          this._children = [];
          this._graphDepth = 0;
  
          this._enabled = true;
          this._enabledInHierarchy = false;
  
          this.scaleCompensation = false;
      }
  
      /**
       * The normalized local space X-axis vector of the graph node in world space.
       *
       * @type {Vec3}
       */
      get right() {
          if (!this._right) {
              this._right = new Vec3();
          }
          return this.getWorldTransform().getX(this._right).normalize();
      }
  
      /**
       * The normalized local space Y-axis vector of the graph node in world space.
       *
       * @type {Vec3}
       */
      get up() {
          if (!this._up) {
              this._up = new Vec3();
          }
          return this.getWorldTransform().getY(this._up).normalize();
      }
  
      /**
       * The normalized local space negative Z-axis vector of the graph node in world space.
       *
       * @type {Vec3}
       */
      get forward() {
          if (!this._forward) {
              this._forward = new Vec3();
          }
          return this.getWorldTransform().getZ(this._forward).normalize().mulScalar(-1);
      }
  
      /**
       * Enable or disable a GraphNode. If one of the GraphNode's parents is disabled there will be
       * no other side effects. If all the parents are enabled then the new value will activate or
       * deactivate all the enabled children of the GraphNode.
       *
       * @type {boolean}
       */
      set enabled(enabled) {
          if (this._enabled !== enabled) {
              this._enabled = enabled;
  
              if (!this._parent || this._parent.enabled)
                  this._notifyHierarchyStateChanged(this, enabled);
          }
      }
  
      get enabled() {
          // make sure to check this._enabled too because if that
          // was false when a parent was updated the _enabledInHierarchy
          // flag may not have been updated for optimization purposes
          return this._enabled && this._enabledInHierarchy;
      }
  
      /**
       * A read-only property to get a parent graph node.
       *
       * @type {GraphNode}
       */
      get parent() {
          return this._parent;
      }
  
      /**
       * A read-only property to get the path of the graph node relative to the root of the hierarchy.
       *
       * @type {string}
       */
      get path() {
          let node = this._parent;
          if (!node) {
              return '';
          }
  
          let result = this.name;
          while (node && node._parent) {
              result = `${node.name}/${result}`;
              node = node._parent;
          }
          return result;
      }
  
      /**
       * A read-only property to get highest graph node from current node.
       *
       * @type {GraphNode}
       */
      get root() {
          let result = this;
          while (result._parent) {
              result = result._parent;
          }
          return result;
      }
  
      /**
       * A read-only property to get the children of this graph node.
       *
       * @type {GraphNode[]}
       */
      get children() {
          return this._children;
      }
  
      /**
       * A read-only property to get the depth of this child within the graph. Note that for
       * performance reasons this is only recalculated when a node is added to a new parent, i.e. It
       * is not recalculated when a node is simply removed from the graph.
       *
       * @type {number}
       */
      get graphDepth() {
          return this._graphDepth;
      }
  
      _notifyHierarchyStateChanged(node, enabled) {
          node._onHierarchyStateChanged(enabled);
  
          const c = node._children;
          for (let i = 0, len = c.length; i < len; i++) {
              if (c[i]._enabled)
                  this._notifyHierarchyStateChanged(c[i], enabled);
          }
      }
  
      /**
       * Called when the enabled flag of the entity or one of its parents changes.
       *
       * @param {boolean} enabled - True if enabled in the hierarchy, false if disabled.
       * @private
       */
      _onHierarchyStateChanged(enabled) {
          // Override in derived classes
          this._enabledInHierarchy = enabled;
          if (enabled && !this._frozen)
              this._unfreezeParentToRoot();
      }
  
      _cloneInternal(clone) {
          clone.name = this.name;
  
          const tags = this.tags._list;
          clone.tags.clear();
          for (let i = 0; i < tags.length; i++)
              clone.tags.add(tags[i]);
  
          clone._labels = Object.assign({}, this._labels);
  
          clone.localPosition.copy(this.localPosition);
          clone.localRotation.copy(this.localRotation);
          clone.localScale.copy(this.localScale);
          clone.localEulerAngles.copy(this.localEulerAngles);
  
          clone.position.copy(this.position);
          clone.rotation.copy(this.rotation);
          clone.eulerAngles.copy(this.eulerAngles);
  
          clone.localTransform.copy(this.localTransform);
          clone._dirtyLocal = this._dirtyLocal;
  
          clone.worldTransform.copy(this.worldTransform);
          clone._dirtyWorld = this._dirtyWorld;
          clone._dirtyNormal = this._dirtyNormal;
          clone._aabbVer = this._aabbVer + 1;
  
          clone._enabled = this._enabled;
  
          clone.scaleCompensation = this.scaleCompensation;
  
          // false as this node is not in the hierarchy yet
          clone._enabledInHierarchy = false;
      }
  
      clone() {
          const clone = new GraphNode();
          this._cloneInternal(clone);
          return clone;
      }
  
      // copies properties from source to this
      copy(source) {
          source._cloneInternal(this);
          return this;
      }
  
      /**
       * Search the graph node and all of its descendants for the nodes that satisfy some search
       * criteria.
       *
       * @param {findNodeCallback|string} attr - This can either be a function or a string. If it's a
       * function, it is executed for each descendant node to test if node satisfies the search
       * logic. Returning true from the function will include the node into the results. If it's a
       * string then it represents the name of a field or a method of the node. If this is the name
       * of a field then the value passed as the second argument will be checked for equality. If
       * this is the name of a function then the return value of the function will be checked for
       * equality against the valued passed as the second argument to this function.
       * @param {object} [value] - If the first argument (attr) is a property name then this value
       * will be checked against the value of the property.
       * @returns {GraphNode[]} The array of graph nodes that match the search criteria.
       * @example
       * // Finds all nodes that have a model component and have `door` in their lower-cased name
       * var doors = house.find(function (node) {
       *     return node.model && node.name.toLowerCase().indexOf('door') !== -1;
       * });
       * @example
       * // Finds all nodes that have the name property set to 'Test'
       * var entities = parent.find('name', 'Test');
       */
      find(attr, value) {
          let result, results = [];
          const len = this._children.length;
  
          if (attr instanceof Function) {
              const fn = attr;
  
              result = fn(this);
              if (result)
                  results.push(this);
  
              for (let i = 0; i < len; i++) {
                  const descendants = this._children[i].find(fn);
                  if (descendants.length)
                      results = results.concat(descendants);
              }
          } else {
              let testValue;
  
              if (this[attr]) {
                  if (this[attr] instanceof Function) {
                      testValue = this[attr]();
                  } else {
                      testValue = this[attr];
                  }
                  if (testValue === value)
                      results.push(this);
              }
  
              for (let i = 0; i < len; ++i) {
                  const descendants = this._children[i].find(attr, value);
                  if (descendants.length)
                      results = results.concat(descendants);
              }
          }
  
          return results;
      }
  
      /**
       * Search the graph node and all of its descendants for the first node that satisfies some
       * search criteria.
       *
       * @param {findNodeCallback|string} attr - This can either be a function or a string. If it's a
       * function, it is executed for each descendant node to test if node satisfies the search
       * logic. Returning true from the function will result in that node being returned from
       * findOne. If it's a string then it represents the name of a field or a method of the node. If
       * this is the name of a field then the value passed as the second argument will be checked for
       * equality. If this is the name of a function then the return value of the function will be
       * checked for equality against the valued passed as the second argument to this function.
       * @param {object} [value] - If the first argument (attr) is a property name then this value
       * will be checked against the value of the property.
       * @returns {GraphNode} A graph node that match the search criteria.
       * @example
       * // Find the first node that is called `head` and has a model component
       * var head = player.findOne(function (node) {
       *     return node.model && node.name === 'head';
       * });
       * @example
       * // Finds the first node that has the name property set to 'Test'
       * var node = parent.findOne('name', 'Test');
       */
      findOne(attr, value) {
          const len = this._children.length;
          let result = null;
  
          if (attr instanceof Function) {
              const fn = attr;
  
              result = fn(this);
              if (result)
                  return this;
  
              for (let i = 0; i < len; i++) {
                  result = this._children[i].findOne(fn);
                  if (result)
                      return result;
              }
          } else {
              let testValue;
              if (this[attr]) {
                  if (this[attr] instanceof Function) {
                      testValue = this[attr]();
                  } else {
                      testValue = this[attr];
                  }
                  if (testValue === value) {
                      return this;
                  }
              }
  
              for (let i = 0; i < len; i++) {
                  result = this._children[i].findOne(attr, value);
                  if (result !== null)
                      return result;
              }
          }
  
          return null;
      }
  
      /**
       * Return all graph nodes that satisfy the search query. Query can be simply a string, or comma
       * separated strings, to have inclusive results of assets that match at least one query. A
       * query that consists of an array of tags can be used to match graph nodes that have each tag
       * of array.
       *
       * @param {string|string[]} query - Name of a tag or array of tags.
       * @returns {GraphNode[]} A list of all graph nodes that match the query.
       * @example
       * // Return all graph nodes that tagged by `animal`
       * var animals = node.findByTag("animal");
       * @example
       * // Return all graph nodes that tagged by `bird` OR `mammal`
       * var birdsAndMammals = node.findByTag("bird", "mammal");
       * @example
       * // Return all assets that tagged by `carnivore` AND `mammal`
       * var meatEatingMammals = node.findByTag(["carnivore", "mammal"]);
       * @example
       * // Return all assets that tagged by (`carnivore` AND `mammal`) OR (`carnivore` AND `reptile`)
       * var meatEatingMammalsAndReptiles = node.findByTag(["carnivore", "mammal"], ["carnivore", "reptile"]);
       */
      findByTag() {
          const tags = this.tags._processArguments(arguments);
          return this._findByTag(tags);
      }
  
      _findByTag(tags) {
          let result = [];
  
          const len = this._children.length;
          for (let i = 0; i < len; i++) {
              if (this._children[i].tags._has(tags))
                  result.push(this._children[i]);
  
              const descendants = this._children[i]._findByTag(tags);
              if (descendants.length)
                  result = result.concat(descendants);
          }
  
          return result;
      }
  
      /**
       * Get the first node found in the graph with the name. The search is depth first.
       *
       * @param {string} name - The name of the graph.
       * @returns {GraphNode} The first node to be found matching the supplied name.
       */
      findByName(name) {
          if (this.name === name) return this;
  
          for (let i = 0; i < this._children.length; i++) {
              const found = this._children[i].findByName(name);
              if (found !== null) return found;
          }
          return null;
      }
  
      /**
       * Get the first node found in the graph by its full path in the graph. The full path has this
       * form 'parent/child/sub-child'. The search is depth first.
       *
       * @param {string|Array} path - The full path of the {@link GraphNode} as either a string or
       * array of {@link GraphNode} names.
       * @returns {GraphNode} The first node to be found matching the supplied path.
       * @example
       * var path = this.entity.findByPath('child/another_child');
       */
      findByPath(path) {
          // accept either string path with '/' separators or array of parts.
          const parts = Array.isArray(path) ? path : path.split('/');
  
          let result = this;
          for (let i = 0, imax = parts.length; i < imax; ++i) {
              result = result.children.find(c => c.name === parts[i]);
              if (!result) {
                  return null;
              }
          }
  
          return result;
      }
  
      /**
       * Executes a provided function once on this graph node and all of its descendants.
       *
       * @param {forEachNodeCallback} callback - The function to execute on the graph node and each
       * descendant.
       * @param {object} [thisArg] - Optional value to use as this when executing callback function.
       * @example
       * // Log the path and name of each node in descendant tree starting with "parent"
       * parent.forEach(function (node) {
       *     console.log(node.path + "/" + node.name);
       * });
       */
      forEach(callback, thisArg) {
          callback.call(thisArg, this);
  
          const children = this._children;
          for (let i = 0; i < children.length; i++) {
              children[i].forEach(callback, thisArg);
          }
      }
  
      /**
       * Check if node is descendant of another node.
       *
       * @param {GraphNode} node - Potential ancestor of node.
       * @returns {boolean} If node is descendant of another node.
       * @example
       * if (roof.isDescendantOf(house)) {
       *     // roof is descendant of house entity
       * }
       */
      isDescendantOf(node) {
          let parent = this._parent;
          while (parent) {
              if (parent === node)
                  return true;
  
              parent = parent._parent;
          }
          return false;
      }
  
      /**
       * Check if node is ancestor for another node.
       *
       * @param {GraphNode} node - Potential descendant of node.
       * @returns {boolean} If node is ancestor for another node.
       * @example
       * if (body.isAncestorOf(foot)) {
       *     // foot is within body's hierarchy
       * }
       */
      isAncestorOf(node) {
          return node.isDescendantOf(this);
      }
  
      /**
       * Get the world space rotation for the specified GraphNode in Euler angle form. The rotation
       * is returned as euler angles in a {@link Vec3}. The value returned by this function should be
       * considered read-only. In order to set the world-space rotation of the graph node, use
       * {@link GraphNode#setEulerAngles}.
       *
       * @returns {Vec3} The world space rotation of the graph node in Euler angle form.
       * @example
       * var angles = this.entity.getEulerAngles();
       * angles.y = 180; // rotate the entity around Y by 180 degrees
       * this.entity.setEulerAngles(angles);
       */
      getEulerAngles() {
          this.getWorldTransform().getEulerAngles(this.eulerAngles);
          return this.eulerAngles;
      }
  
      /**
       * Get the rotation in local space for the specified GraphNode. The rotation is returned as
       * euler angles in a {@link Vec3}. The returned vector should be considered read-only. To
       * update the local rotation, use {@link GraphNode#setLocalEulerAngles}.
       *
       * @returns {Vec3} The local space rotation of the graph node as euler angles in XYZ order.
       * @example
       * var angles = this.entity.getLocalEulerAngles();
       * angles.y = 180;
       * this.entity.setLocalEulerAngles(angles);
       */
      getLocalEulerAngles() {
          this.localRotation.getEulerAngles(this.localEulerAngles);
          return this.localEulerAngles;
      }
  
      /**
       * Get the position in local space for the specified GraphNode. The position is returned as a
       * {@link Vec3}. The returned vector should be considered read-only. To update the local
       * position, use {@link GraphNode#setLocalPosition}.
       *
       * @returns {Vec3} The local space position of the graph node.
       * @example
       * var position = this.entity.getLocalPosition();
       * position.x += 1; // move the entity 1 unit along x.
       * this.entity.setLocalPosition(position);
       */
      getLocalPosition() {
          return this.localPosition;
      }
  
      /**
       * Get the rotation in local space for the specified GraphNode. The rotation is returned as a
       * {@link Quat}. The returned quaternion should be considered read-only. To update the local
       * rotation, use {@link GraphNode#setLocalRotation}.
       *
       * @returns {Quat} The local space rotation of the graph node as a quaternion.
       * @example
       * var rotation = this.entity.getLocalRotation();
       */
      getLocalRotation() {
          return this.localRotation;
      }
  
      /**
       * Get the scale in local space for the specified GraphNode. The scale is returned as a
       * {@link Vec3}. The returned vector should be considered read-only. To update the local scale,
       * use {@link GraphNode#setLocalScale}.
       *
       * @returns {Vec3} The local space scale of the graph node.
       * @example
       * var scale = this.entity.getLocalScale();
       * scale.x = 100;
       * this.entity.setLocalScale(scale);
       */
      getLocalScale() {
          return this.localScale;
      }
  
      /**
       * Get the local transform matrix for this graph node. This matrix is the transform relative to
       * the node's parent's world transformation matrix.
       *
       * @returns {Mat4} The node's local transformation matrix.
       * @example
       * var transform = this.entity.getLocalTransform();
       */
      getLocalTransform() {
          if (this._dirtyLocal) {
              this.localTransform.setTRS(this.localPosition, this.localRotation, this.localScale);
              this._dirtyLocal = false;
          }
          return this.localTransform;
      }
  
      /**
       * Get the world space position for the specified GraphNode. The position is returned as a
       * {@link Vec3}. The value returned by this function should be considered read-only. In order
       * to set the world-space position of the graph node, use {@link GraphNode#setPosition}.
       *
       * @returns {Vec3} The world space position of the graph node.
       * @example
       * var position = this.entity.getPosition();
       * position.x = 10;
       * this.entity.setPosition(position);
       */
      getPosition() {
          this.getWorldTransform().getTranslation(this.position);
          return this.position;
      }
  
      /**
       * Get the world space rotation for the specified GraphNode. The rotation is returned as a
       * {@link Quat}. The value returned by this function should be considered read-only. In order
       * to set the world-space rotation of the graph node, use {@link GraphNode#setRotation}.
       *
       * @returns {Quat} The world space rotation of the graph node as a quaternion.
       * @example
       * var rotation = this.entity.getRotation();
       */
      getRotation() {
          this.rotation.setFromMat4(this.getWorldTransform());
          return this.rotation;
      }
  
      /**
       * Get the world space scale for the specified GraphNode. The returned value will only be
       * correct for graph nodes that have a non-skewed world transform (a skew can be introduced by
       * the compounding of rotations and scales higher in the graph node hierarchy). The scale is
       * returned as a {@link Vec3}. The value returned by this function should be considered
       * read-only. Note that it is not possible to set the world space scale of a graph node
       * directly.
       *
       * @returns {Vec3} The world space scale of the graph node.
       * @example
       * var scale = this.entity.getScale();
       * @private
       */
      getScale() {
          if (!this._scale) {
              this._scale = new Vec3();
          }
          return this.getWorldTransform().getScale(this._scale);
      }
  
      /**
       * Get the world transformation matrix for this graph node.
       *
       * @returns {Mat4} The node's world transformation matrix.
       * @example
       * var transform = this.entity.getWorldTransform();
       */
      getWorldTransform() {
          if (!this._dirtyLocal && !this._dirtyWorld)
              return this.worldTransform;
  
          if (this._parent)
              this._parent.getWorldTransform();
  
          this._sync();
  
          return this.worldTransform;
      }
  
      /**
       * Remove graph node from current parent and add as child to new parent.
       *
       * @param {GraphNode} parent - New parent to attach graph node to.
       * @param {number} [index] - The child index where the child node should be placed.
       */
      reparent(parent, index) {
          const current = this._parent;
  
          if (current)
              current.removeChild(this);
  
          if (parent) {
              if (index >= 0) {
                  parent.insertChild(this, index);
              } else {
                  parent.addChild(this);
              }
          }
      }
  
      /**
       * Sets the local-space rotation of the specified graph node using euler angles. Eulers are
       * interpreted in XYZ order. Eulers must be specified in degrees. This function has two valid
       * signatures: you can either pass a 3D vector or 3 numbers to specify the local-space euler
       * rotation.
       *
       * @param {Vec3|number} x - 3-dimensional vector holding eulers or rotation around local-space
       * x-axis in degrees.
       * @param {number} [y] - Rotation around local-space y-axis in degrees.
       * @param {number} [z] - Rotation around local-space z-axis in degrees.
       * @example
       * // Set rotation of 90 degrees around y-axis via 3 numbers
       * this.entity.setLocalEulerAngles(0, 90, 0);
       * @example
       * // Set rotation of 90 degrees around y-axis via a vector
       * var angles = new pc.Vec3(0, 90, 0);
       * this.entity.setLocalEulerAngles(angles);
       */
      setLocalEulerAngles(x, y, z) {
          if (x instanceof Vec3) {
              this.localRotation.setFromEulerAngles(x.x, x.y, x.z);
          } else {
              this.localRotation.setFromEulerAngles(x, y, z);
          }
  
          if (!this._dirtyLocal)
              this._dirtifyLocal();
      }
  
      /**
       * Sets the local-space position of the specified graph node. This function has two valid
       * signatures: you can either pass a 3D vector or 3 numbers to specify the local-space
       * position.
       *
       * @param {Vec3|number} x - 3-dimensional vector holding local-space position or
       * x-coordinate of local-space position.
       * @param {number} [y] - Y-coordinate of local-space position.
       * @param {number} [z] - Z-coordinate of local-space position.
       * @example
       * // Set via 3 numbers
       * this.entity.setLocalPosition(0, 10, 0);
       * @example
       * // Set via vector
       * var pos = new pc.Vec3(0, 10, 0);
       * this.entity.setLocalPosition(pos);
       */
      setLocalPosition(x, y, z) {
          if (x instanceof Vec3) {
              this.localPosition.copy(x);
          } else {
              this.localPosition.set(x, y, z);
          }
  
          if (!this._dirtyLocal)
              this._dirtifyLocal();
      }
  
      /**
       * Sets the local-space rotation of the specified graph node. This function has two valid
       * signatures: you can either pass a quaternion or 3 numbers to specify the local-space
       * rotation.
       *
       * @param {Quat|number} x - Quaternion holding local-space rotation or x-component of
       * local-space quaternion rotation.
       * @param {number} [y] - Y-component of local-space quaternion rotation.
       * @param {number} [z] - Z-component of local-space quaternion rotation.
       * @param {number} [w] - W-component of local-space quaternion rotation.
       * @example
       * // Set via 4 numbers
       * this.entity.setLocalRotation(0, 0, 0, 1);
       * @example
       * // Set via quaternion
       * var q = pc.Quat();
       * this.entity.setLocalRotation(q);
       */
      setLocalRotation(x, y, z, w) {
          if (x instanceof Quat) {
              this.localRotation.copy(x);
          } else {
              this.localRotation.set(x, y, z, w);
          }
  
          if (!this._dirtyLocal)
              this._dirtifyLocal();
      }
  
      /**
       * Sets the local-space scale factor of the specified graph node. This function has two valid
       * signatures: you can either pass a 3D vector or 3 numbers to specify the local-space scale.
       *
       * @param {Vec3|number} x - 3-dimensional vector holding local-space scale or x-coordinate
       * of local-space scale.
       * @param {number} [y] - Y-coordinate of local-space scale.
       * @param {number} [z] - Z-coordinate of local-space scale.
       * @example
       * // Set via 3 numbers
       * this.entity.setLocalScale(10, 10, 10);
       * @example
       * // Set via vector
       * var scale = new pc.Vec3(10, 10, 10);
       * this.entity.setLocalScale(scale);
       */
      setLocalScale(x, y, z) {
          if (x instanceof Vec3) {
              this.localScale.copy(x);
          } else {
              this.localScale.set(x, y, z);
          }
  
          if (!this._dirtyLocal)
              this._dirtifyLocal();
      }
  
      _dirtifyLocal() {
          if (!this._dirtyLocal) {
              this._dirtyLocal = true;
              if (!this._dirtyWorld)
                  this._dirtifyWorld();
          }
      }
  
      _unfreezeParentToRoot() {
          let p = this._parent;
          while (p) {
              p._frozen = false;
              p = p._parent;
          }
      }
  
      _dirtifyWorld() {
          if (!this._dirtyWorld)
              this._unfreezeParentToRoot();
          this._dirtifyWorldInternal();
      }
  
      _dirtifyWorldInternal() {
          if (!this._dirtyWorld) {
              this._frozen = false;
              this._dirtyWorld = true;
              for (let i = 0; i < this._children.length; i++) {
                  if (!this._children[i]._dirtyWorld)
                      this._children[i]._dirtifyWorldInternal();
              }
          }
          this._dirtyNormal = true;
          this._aabbVer++;
      }
  
      /**
       * Sets the world-space position of the specified graph node. This function has two valid
       * signatures: you can either pass a 3D vector or 3 numbers to specify the world-space
       * position.
       *
       * @param {Vec3|number} x - 3-dimensional vector holding world-space position or
       * x-coordinate of world-space position.
       * @param {number} [y] - Y-coordinate of world-space position.
       * @param {number} [z] - Z-coordinate of world-space position.
       * @example
       * // Set via 3 numbers
       * this.entity.setPosition(0, 10, 0);
       * @example
       * // Set via vector
       * var position = new pc.Vec3(0, 10, 0);
       * this.entity.setPosition(position);
       */
      setPosition(x, y, z) {
          if (x instanceof Vec3) {
              position.copy(x);
          } else {
              position.set(x, y, z);
          }
  
          if (this._parent === null) {
              this.localPosition.copy(position);
          } else {
              invParentWtm.copy(this._parent.getWorldTransform()).invert();
              invParentWtm.transformPoint(position, this.localPosition);
          }
  
          if (!this._dirtyLocal)
              this._dirtifyLocal();
      }
  
      /**
       * Sets the world-space rotation of the specified graph node. This function has two valid
       * signatures: you can either pass a quaternion or 3 numbers to specify the world-space
       * rotation.
       *
       * @param {Quat|number} x - Quaternion holding world-space rotation or x-component of
       * world-space quaternion rotation.
       * @param {number} [y] - Y-component of world-space quaternion rotation.
       * @param {number} [z] - Z-component of world-space quaternion rotation.
       * @param {number} [w] - W-component of world-space quaternion rotation.
       * @example
       * // Set via 4 numbers
       * this.entity.setRotation(0, 0, 0, 1);
       * @example
       * // Set via quaternion
       * var q = pc.Quat();
       * this.entity.setRotation(q);
       */
      setRotation(x, y, z, w) {
          if (x instanceof Quat) {
              rotation.copy(x);
          } else {
              rotation.set(x, y, z, w);
          }
  
          if (this._parent === null) {
              this.localRotation.copy(rotation);
          } else {
              const parentRot = this._parent.getRotation();
              invParentRot.copy(parentRot).invert();
              this.localRotation.copy(invParentRot).mul(rotation);
          }
  
          if (!this._dirtyLocal)
              this._dirtifyLocal();
      }
  
      /**
       * Sets the world-space rotation of the specified graph node using euler angles. Eulers are
       * interpreted in XYZ order. Eulers must be specified in degrees. This function has two valid
       * signatures: you can either pass a 3D vector or 3 numbers to specify the world-space euler
       * rotation.
       *
       * @param {Vec3|number} x - 3-dimensional vector holding eulers or rotation around world-space
       * x-axis in degrees.
       * @param {number} [y] - Rotation around world-space y-axis in degrees.
       * @param {number} [z] - Rotation around world-space z-axis in degrees.
       * @example
       * // Set rotation of 90 degrees around world-space y-axis via 3 numbers
       * this.entity.setEulerAngles(0, 90, 0);
       * @example
       * // Set rotation of 90 degrees around world-space y-axis via a vector
       * var angles = new pc.Vec3(0, 90, 0);
       * this.entity.setEulerAngles(angles);
       */
      setEulerAngles(x, y, z) {
          if (x instanceof Vec3) {
              this.localRotation.setFromEulerAngles(x.x, x.y, x.z);
          } else {
              this.localRotation.setFromEulerAngles(x, y, z);
          }
  
          if (this._parent !== null) {
              const parentRot = this._parent.getRotation();
              invParentRot.copy(parentRot).invert();
              this.localRotation.mul2(invParentRot, this.localRotation);
          }
  
          if (!this._dirtyLocal)
              this._dirtifyLocal();
      }
  
      /**
       * Add a new child to the child list and update the parent value of the child node.
       *
       * @param {GraphNode} node - The new child to add.
       * @example
       * var e = new pc.Entity(app);
       * this.entity.addChild(e);
       */
      addChild(node) {
          if (node._parent !== null)
              throw new Error("GraphNode is already parented");
  
          // #if _DEBUG
          this._debugInsertChild(node);
          // #endif
  
          this._children.push(node);
          this._onInsertChild(node);
      }
  
      addChildAndSaveTransform(node) {
          // #if _DEBUG
          this._debugInsertChild(node);
          // #endif
  
          const wPos = node.getPosition();
          const wRot = node.getRotation();
  
          const current = node._parent;
          if (current)
              current.removeChild(node);
  
          node.setPosition(tmpMat4.copy(this.worldTransform).invert().transformPoint(wPos));
          node.setRotation(tmpQuat.copy(this.getRotation()).invert().mul(wRot));
  
          this._children.push(node);
          this._onInsertChild(node);
      }
  
      /**
       * Insert a new child to the child list at the specified index and update the parent value of
       * the child node.
       *
       * @param {GraphNode} node - The new child to insert.
       * @param {number} index - The index in the child list of the parent where the new node will be
       * inserted.
       * @example
       * var e = new pc.Entity(app);
       * this.entity.insertChild(e, 1);
       */
      insertChild(node, index) {
          if (node._parent !== null)
              throw new Error("GraphNode is already parented");
  
          // #if _DEBUG
          this._debugInsertChild(node);
          // #endif
  
          this._children.splice(index, 0, node);
          this._onInsertChild(node);
      }
  
      // #if _DEBUG
      _debugInsertChild(node) {
          if (this === node)
              throw new Error("GraphNode cannot be a child of itself");
  
          if (this.isDescendantOf(node))
              throw new Error("GraphNode cannot add an ancestor as a child");
      }
      // #endif
  
      // fires an event on all children of the node
      // the event name is fired on the first (root) node only
      // the event nameHierarchy is fired for all children
      _fireOnHierarchy(name, nameHierarchy, parent) {
          this.fire(name, parent);
          for (let i = 0; i < this._children.length; i++) {
              this._children[i]._fireOnHierarchy(nameHierarchy, nameHierarchy, parent);
          }
      }
  
      _onInsertChild(node) {
          node._parent = this;
  
          // the child node should be enabled in the hierarchy only if itself is enabled and if
          // this parent is enabled
          const enabledInHierarchy = (node._enabled && this.enabled);
          if (node._enabledInHierarchy !== enabledInHierarchy) {
              node._enabledInHierarchy = enabledInHierarchy;
  
              // propagate the change to the children - necessary if we reparent a node
              // under a parent with a different enabled state (if we reparent a node that is
              // not active in the hierarchy under a parent who is active in the hierarchy then
              // we want our node to be activated)
              node._notifyHierarchyStateChanged(node, enabledInHierarchy);
          }
  
          // The graph depth of the child and all of its descendants will now change
          node._updateGraphDepth();
  
          // The child (plus subhierarchy) will need world transforms to be recalculated
          node._dirtifyWorld();
          // node might be already marked as dirty, in that case the whole chain stays frozen, so let's enforce unfreeze
          if (this._frozen)
              node._unfreezeParentToRoot();
  
          // alert an entity hierarchy that it has been inserted
          node._fireOnHierarchy('insert', 'inserthierarchy', this);
  
          // alert the parent that it has had a child inserted
          if (this.fire) this.fire('childinsert', node);
      }
  
      _updateGraphDepth() {
          if (this._parent) {
              this._graphDepth = this._parent._graphDepth + 1;
          } else {
              this._graphDepth = 0;
          }
  
          for (let i = 0, len = this._children.length; i < len; i++) {
              this._children[i]._updateGraphDepth();
          }
      }
  
      /**
       * Remove the node from the child list and update the parent value of the child.
       *
       * @param {GraphNode} child - The node to remove.
       * @example
       * var child = this.entity.children[0];
       * this.entity.removeChild(child);
       */
      removeChild(child) {
  
          // Remove from child list
          const length = this._children.length;
          for (let i = 0; i < length; ++i) {
              if (this._children[i] === child) {
                  this._children.splice(i, 1);
  
                  // Clear parent
                  child._parent = null;
  
                  // alert children that they has been removed
                  child._fireOnHierarchy('remove', 'removehierarchy', this);
  
                  // alert the parent that it has had a child removed
                  if (this.fire) this.fire('childremove', child);
  
                  return;
              }
          }
      }
  
      _sync() {
          if (this._dirtyLocal) {
              this.localTransform.setTRS(this.localPosition, this.localRotation, this.localScale);
  
              this._dirtyLocal = false;
          }
  
          if (this._dirtyWorld) {
              if (this._parent === null) {
                  this.worldTransform.copy(this.localTransform);
              } else {
                  if (this.scaleCompensation) {
                      let parentWorldScale;
                      const parent = this._parent;
  
                      // Find a parent of the first uncompensated node up in the hierarchy and use its scale * localScale
                      let scale = this.localScale;
                      let parentToUseScaleFrom = parent; // current parent
                      if (parentToUseScaleFrom) {
                          while (parentToUseScaleFrom && parentToUseScaleFrom.scaleCompensation) {
                              parentToUseScaleFrom = parentToUseScaleFrom._parent;
                          }
                          // topmost node with scale compensation
                          if (parentToUseScaleFrom) {
                              parentToUseScaleFrom = parentToUseScaleFrom._parent; // node without scale compensation
                              if (parentToUseScaleFrom) {
                                  parentWorldScale = parentToUseScaleFrom.worldTransform.getScale();
                                  scaleCompensateScale.mul2(parentWorldScale, this.localScale);
                                  scale = scaleCompensateScale;
                              }
                          }
                      }
  
                      // Rotation is as usual
                      scaleCompensateRot2.setFromMat4(parent.worldTransform);
                      scaleCompensateRot.mul2(scaleCompensateRot2, this.localRotation);
  
                      // Find matrix to transform position
                      let tmatrix = parent.worldTransform;
                      if (parent.scaleCompensation) {
                          scaleCompensateScaleForParent.mul2(parentWorldScale, parent.getLocalScale());
                          scaleCompensatePosTransform.setTRS(parent.worldTransform.getTranslation(scaleCompensatePos),
                                                             scaleCompensateRot2,
                                                             scaleCompensateScaleForParent);
                          tmatrix = scaleCompensatePosTransform;
                      }
                      tmatrix.transformPoint(this.localPosition, scaleCompensatePos);
  
                      this.worldTransform.setTRS(scaleCompensatePos, scaleCompensateRot, scale);
  
                  } else {
                      this.worldTransform.mulAffine2(this._parent.worldTransform, this.localTransform);
                  }
              }
  
              this._dirtyWorld = false;
          }
      }
  
      /**
       * Updates the world transformation matrices at this node and all of its descendants.
       *
       * @private
       */
      syncHierarchy() {
          if (!this._enabled)
              return;
  
          if (this._frozen)
              return;
          this._frozen = true;
  
          if (this._dirtyLocal || this._dirtyWorld) {
              this._sync();
          }
  
          const children = this._children;
          for (let i = 0, len = children.length; i < len; i++) {
              children[i].syncHierarchy();
          }
      }
  
      /**
       * Reorients the graph node so that the negative z-axis points towards the target. This
       * function has two valid signatures. Either pass 3D vectors for the look at coordinate and up
       * vector, or pass numbers to represent the vectors.
       *
       * @param {Vec3|number} x - If passing a 3D vector, this is the world-space coordinate to look at.
       * Otherwise, it is the x-component of the world-space coordinate to look at.
       * @param {Vec3|number} [y] - If passing a 3D vector, this is the world-space up vector for look at
       * transform. Otherwise, it is the y-component of the world-space coordinate to look at.
       * @param {number} [z] - Z-component of the world-space coordinate to look at.
       * @param {number} [ux=0] - X-component of the up vector for the look at transform.
       * @param {number} [uy=1] - Y-component of the up vector for the look at transform.
       * @param {number} [uz=0] - Z-component of the up vector for the look at transform.
       * @example
       * // Look at another entity, using the (default) positive y-axis for up
       * var position = otherEntity.getPosition();
       * this.entity.lookAt(position);
       * @example
       * // Look at another entity, using the negative world y-axis for up
       * var position = otherEntity.getPosition();
       * this.entity.lookAt(position, pc.Vec3.DOWN);
       * @example
       * // Look at the world space origin, using the (default) positive y-axis for up
       * this.entity.lookAt(0, 0, 0);
       * @example
       * // Look at world-space coordinate [10, 10, 10], using the negative world y-axis for up
       * this.entity.lookAt(10, 10, 10, 0, -1, 0);
       */
      lookAt(x, y, z, ux = 0, uy = 1, uz = 0) {
          if (x instanceof Vec3) {
              target.copy(x);
  
              if (y instanceof Vec3) { // vec3, vec3
                  up.copy(y);
              } else { // vec3
                  up.copy(Vec3.UP);
              }
          } else if (z === undefined) {
              return;
          } else {
              target.set(x, y, z);
              up.set(ux, uy, uz);
          }
  
          matrix.setLookAt(this.getPosition(), target, up);
          rotation.setFromMat4(matrix);
          this.setRotation(rotation);
      }
  
      /**
       * Translates the graph node in world-space by the specified translation vector. This function
       * has two valid signatures: you can either pass a 3D vector or 3 numbers to specify the
       * world-space translation.
       *
       * @param {Vec3|number} x - 3-dimensional vector holding world-space translation or
       * x-coordinate of world-space translation.
       * @param {number} [y] - Y-coordinate of world-space translation.
       * @param {number} [z] - Z-coordinate of world-space translation.
       * @example
       * // Translate via 3 numbers
       * this.entity.translate(10, 0, 0);
       * @example
       * // Translate via vector
       * var t = new pc.Vec3(10, 0, 0);
       * this.entity.translate(t);
       */
      translate(x, y, z) {
          if (x instanceof Vec3) {
              position.copy(x);
          } else {
              position.set(x, y, z);
          }
  
          position.add(this.getPosition());
          this.setPosition(position);
      }
  
      /**
       * Translates the graph node in local-space by the specified translation vector. This function
       * has two valid signatures: you can either pass a 3D vector or 3 numbers to specify the
       * local-space translation.
       *
       * @param {Vec3|number} x - 3-dimensional vector holding local-space translation or
       * x-coordinate of local-space translation.
       * @param {number} [y] - Y-coordinate of local-space translation.
       * @param {number} [z] - Z-coordinate of local-space translation.
       * @example
       * // Translate via 3 numbers
       * this.entity.translateLocal(10, 0, 0);
       * @example
       * // Translate via vector
       * var t = new pc.Vec3(10, 0, 0);
       * this.entity.translateLocal(t);
       */
      translateLocal(x, y, z) {
          if (x instanceof Vec3) {
              position.copy(x);
          } else {
              position.set(x, y, z);
          }
  
          this.localRotation.transformVector(position, position);
          this.localPosition.add(position);
  
          if (!this._dirtyLocal)
              this._dirtifyLocal();
      }
  
      /**
       * Rotates the graph node in world-space by the specified Euler angles. Eulers are specified in
       * degrees in XYZ order. This function has two valid signatures: you can either pass a 3D
       * vector or 3 numbers to specify the world-space rotation.
       *
       * @param {Vec3|number} x - 3-dimensional vector holding world-space rotation or
       * rotation around world-space x-axis in degrees.
       * @param {number} [y] - Rotation around world-space y-axis in degrees.
       * @param {number} [z] - Rotation around world-space z-axis in degrees.
       * @example
       * // Rotate via 3 numbers
       * this.entity.rotate(0, 90, 0);
       * @example
       * // Rotate via vector
       * var r = new pc.Vec3(0, 90, 0);
       * this.entity.rotate(r);
       */
      rotate(x, y, z) {
          if (x instanceof Vec3) {
              rotation.setFromEulerAngles(x.x, x.y, x.z);
          } else {
              rotation.setFromEulerAngles(x, y, z);
          }
  
          if (this._parent === null) {
              this.localRotation.mul2(rotation, this.localRotation);
          } else {
              const rot = this.getRotation();
              const parentRot = this._parent.getRotation();
  
              invParentRot.copy(parentRot).invert();
              rotation.mul2(invParentRot, rotation);
              this.localRotation.mul2(rotation, rot);
          }
  
          if (!this._dirtyLocal)
              this._dirtifyLocal();
      }
  
      /**
       * Rotates the graph node in local-space by the specified Euler angles. Eulers are specified in
       * degrees in XYZ order. This function has two valid signatures: you can either pass a 3D
       * vector or 3 numbers to specify the local-space rotation.
       *
       * @param {Vec3|number} x - 3-dimensional vector holding local-space rotation or
       * rotation around local-space x-axis in degrees.
       * @param {number} [y] - Rotation around local-space y-axis in degrees.
       * @param {number} [z] - Rotation around local-space z-axis in degrees.
       * @example
       * // Rotate via 3 numbers
       * this.entity.rotateLocal(0, 90, 0);
       * @example
       * // Rotate via vector
       * var r = new pc.Vec3(0, 90, 0);
       * this.entity.rotateLocal(r);
       */
      rotateLocal(x, y, z) {
          if (x instanceof Vec3) {
              rotation.setFromEulerAngles(x.x, x.y, x.z);
          } else {
              rotation.setFromEulerAngles(x, y, z);
          }
  
          this.localRotation.mul(rotation);
  
          if (!this._dirtyLocal)
              this._dirtifyLocal();
      }
  }
  
  export { GraphNode };
  