text: "use strict";
  
  //  nearActionGrabEntity.js
  //
  //  Distributed under the Apache License, Version 2.0.
  //  See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
  
  /* global Script, Entities, MyAvatar, Controller, RIGHT_HAND, LEFT_HAND,
     getControllerJointIndex, getGrabbableData, enableDispatcherModule, disableDispatcherModule,
     propsArePhysical, Messages, HAPTIC_PULSE_STRENGTH, HAPTIC_PULSE_DURATION, entityIsGrabbable,
     Quat, Vec3, MSECS_PER_SEC, getControllerWorldLocation, makeDispatcherModuleParameters, makeRunningValues,
     TRIGGER_OFF_VALUE, NEAR_GRAB_RADIUS, findGroupParent, entityIsCloneable, propsAreCloneDynamic, cloneEntity,
     HAPTIC_PULSE_STRENGTH, HAPTIC_PULSE_DURATION, BUMPER_ON_VALUE, unhighlightTargetEntity, Uuid
  */
  
  Script.include("/~/system/libraries/controllerDispatcherUtils.js");
  Script.include("/~/system/libraries/controllers.js");
  Script.include("/~/system/libraries/cloneEntityUtils.js");
  
  (function() {
  
      function NearActionGrabEntity(hand) {
          this.hand = hand;
          this.targetEntityID = null;
          this.actionID = null; // action this script created...
          this.hapticTargetID = null;
  
          this.parameters = makeDispatcherModuleParameters(
              500,
              this.hand === RIGHT_HAND ? ["rightHand"] : ["leftHand"],
              [],
              100);
  
          var NEAR_GRABBING_ACTION_TIMEFRAME = 0.05; // how quickly objects move to their new position
          var ACTION_TTL = 15; // seconds
          var ACTION_TTL_REFRESH = 5;
  
          // XXX does handJointIndex change if the avatar changes?
          this.handJointIndex = MyAvatar.getJointIndex(this.hand === RIGHT_HAND ? "RightHand" : "LeftHand");
          this.controllerJointIndex = getControllerJointIndex(this.hand);
  
  
          // handPosition is where the avatar's hand appears to be, in-world.
          this.getHandPosition = function () {
              if (this.hand === RIGHT_HAND) {
                  return MyAvatar.getRightPalmPosition();
              } else {
                  return MyAvatar.getLeftPalmPosition();
              }
          };
  
          this.getHandRotation = function () {
              if (this.hand === RIGHT_HAND) {
                  return MyAvatar.getRightPalmRotation();
              } else {
                  return MyAvatar.getLeftPalmRotation();
              }
          };
  
  
          this.startNearGrabAction = function (controllerData, targetProps) {
              Controller.triggerHapticPulse(HAPTIC_PULSE_STRENGTH, HAPTIC_PULSE_DURATION, this.hand);
  
              var grabbableData = getGrabbableData(targetProps);
              this.ignoreIK = grabbableData.ignoreIK;
              this.kinematicGrab = grabbableData.kinematic;
  
              var handRotation;
              var handPosition;
              if (this.ignoreIK) {
                  var controllerID =
                      (this.hand === RIGHT_HAND) ? Controller.Standard.RightHand : Controller.Standard.LeftHand;
                  var controllerLocation = getControllerWorldLocation(controllerID, false);
                  handRotation = controllerLocation.orientation;
                  handPosition = controllerLocation.position;
              } else {
                  handRotation = this.getHandRotation();
                  handPosition = this.getHandPosition();
              }
  
              var objectRotation = targetProps.rotation;
              this.offsetRotation = Quat.multiply(Quat.inverse(handRotation), objectRotation);
  
              var currentObjectPosition = targetProps.position;
              var offset = Vec3.subtract(currentObjectPosition, handPosition);
              this.offsetPosition = Vec3.multiplyQbyV(Quat.inverse(Quat.multiply(handRotation, this.offsetRotation)), offset);
  
              var now = Date.now();
              this.actionTimeout = now + (ACTION_TTL * MSECS_PER_SEC);
  
              if (this.actionID) {
                  Entities.deleteAction(this.targetEntityID, this.actionID);
              }
              this.actionID = Entities.addAction("hold", this.targetEntityID, {
                  hand: this.hand === RIGHT_HAND ? "right" : "left",
                  timeScale: NEAR_GRABBING_ACTION_TIMEFRAME,
                  relativePosition: this.offsetPosition,
                  relativeRotation: this.offsetRotation,
                  ttl: ACTION_TTL,
                  kinematic: this.kinematicGrab,
                  kinematicSetVelocity: true,
                  ignoreIK: this.ignoreIK
              });
              if (this.actionID === Uuid.NULL) {
                  this.actionID = null;
                  return;
              }
  
              Messages.sendMessage('Hifi-Object-Manipulation', JSON.stringify({
                  action: 'grab',
                  grabbedEntity: this.targetEntityID,
                  joint: this.hand === RIGHT_HAND ? "RightHand" : "LeftHand"
              }));
  
              var args = [this.hand === RIGHT_HAND ? "right" : "left", MyAvatar.sessionUUID];
              Entities.callEntityMethod(this.targetEntityID, "startNearGrab", args);
              unhighlightTargetEntity(this.targetEntityID);
              var message = {
                  hand: this.hand,
                  entityID: this.targetEntityID
              };
  
              Messages.sendLocalMessage('Hifi-unhighlight-entity', JSON.stringify(message));
          };
  
          // this is for when the action is going to time-out
          this.refreshNearGrabAction = function (controllerData) {
              var now = Date.now();
              if (this.actionID && this.actionTimeout - now < ACTION_TTL_REFRESH * MSECS_PER_SEC) {
                  // if less than a 5 seconds left, refresh the actions ttl
                  var success = Entities.updateAction(this.targetEntityID, this.actionID, {
                      hand: this.hand === RIGHT_HAND ? "right" : "left",
                      timeScale: NEAR_GRABBING_ACTION_TIMEFRAME,
                      relativePosition: this.offsetPosition,
                      relativeRotation: this.offsetRotation,
                      ttl: ACTION_TTL,
                      kinematic: this.kinematicGrab,
                      kinematicSetVelocity: true,
                      ignoreIK: this.ignoreIK
                  });
                  if (success) {
                      this.actionTimeout = now + (ACTION_TTL * MSECS_PER_SEC);
                  }
              }
          };
  
          this.endNearGrabAction = function () {
              var args = [this.hand === RIGHT_HAND ? "right" : "left", MyAvatar.sessionUUID];
              Entities.callEntityMethod(this.targetEntityID, "releaseGrab", args);
  
              Entities.deleteAction(this.targetEntityID, this.actionID);
              this.actionID = null;
  
              Messages.sendMessage('Hifi-Object-Manipulation', JSON.stringify({
                  action: 'release',
                  grabbedEntity: this.targetEntityID,
                  joint: this.hand === RIGHT_HAND ? "RightHand" : "LeftHand"
              }));
  
              this.targetEntityID = null;
          };
  
          this.getTargetProps = function (controllerData) {
              // nearbyEntityProperties is already sorted by distance from controller
              var nearbyEntityProperties = controllerData.nearbyEntityProperties[this.hand];
              var sensorScaleFactor = MyAvatar.sensorToWorldScale;
              for (var i = 0; i < nearbyEntityProperties.length; i++) {
                  var props = nearbyEntityProperties[i];
                  if (props.distance > NEAR_GRAB_RADIUS * sensorScaleFactor) {
                      break;
                  }
                  if (entityIsGrabbable(props) || entityIsCloneable(props)) {
                      if (props.id !== this.hapticTargetID) {
                          Controller.triggerHapticPulse(HAPTIC_PULSE_STRENGTH, HAPTIC_PULSE_DURATION, this.hand);
                          this.hapticTargetID = props.id;
                      }
                      // if we've attempted to grab a child, roll up to the root of the tree
                      var groupRootProps = findGroupParent(controllerData, props);
                      if (entityIsGrabbable(groupRootProps)) {
                          return groupRootProps;
                      }
                      return props;
                  }
              }
              return null;
          };
  
          this.isReady = function (controllerData) {
              this.targetEntityID = null;
  
              var targetProps = this.getTargetProps(controllerData);
              if (controllerData.triggerValues[this.hand] < TRIGGER_OFF_VALUE &&
                  controllerData.secondaryValues[this.hand] < TRIGGER_OFF_VALUE) {
                  return makeRunningValues(false, [], []);
              }
  
              if (targetProps) {
                  if ((!propsArePhysical(targetProps) && !propsAreCloneDynamic(targetProps)) ||
                      targetProps.parentID !== Uuid.NULL) {
                      return makeRunningValues(false, [], []); // let nearParentGrabEntity handle it
                  } else {
                      this.targetEntityID = targetProps.id;
                      return makeRunningValues(true, [this.targetEntityID], []);
                  }
              } else {
                  this.hapticTargetID = null;
                  return makeRunningValues(false, [], []);
              }
          };
  
          this.run = function (controllerData) {
              if (this.actionID) {
                  if (controllerData.triggerClicks[this.hand] < TRIGGER_OFF_VALUE &&
                      controllerData.secondaryValues[this.hand] < TRIGGER_OFF_VALUE) {
                      this.endNearGrabAction();
                      this.hapticTargetID = null;
                      return makeRunningValues(false, [], []);
                  }
  
                  this.refreshNearGrabAction(controllerData);
                  var args = [this.hand === RIGHT_HAND ? "right" : "left", MyAvatar.sessionUUID];
                  Entities.callEntityMethod(this.targetEntityID, "continueNearGrab", args);
              } else {
  
                  // still searching / highlighting
                  var readiness = this.isReady (controllerData);
                  if (!readiness.active) {
                      return readiness;
                  }
  
                  var targetProps = this.getTargetProps(controllerData);
                  if (targetProps) {
                      if (controllerData.triggerClicks[this.hand] ||
                          controllerData.secondaryValues[this.hand] > BUMPER_ON_VALUE) {
                          // switch to grabbing
                          var targetCloneable = entityIsCloneable(targetProps);
                          if (targetCloneable) {
                              var cloneID = cloneEntity(targetProps);
                              var cloneProps = Entities.getEntityProperties(cloneID);
                              this.targetEntityID = cloneID;
                              this.startNearGrabAction(controllerData, cloneProps);
                          } else {
                              this.startNearGrabAction(controllerData, targetProps);
                          }
                      }
                  }
              }
  
              return makeRunningValues(true, [this.targetEntityID], []);
          };
  
          this.cleanup = function () {
              if (this.targetEntityID) {
                  this.endNearGrabAction();
              }
          };
      }
  
      var leftNearActionGrabEntity = new NearActionGrabEntity(LEFT_HAND);
      var rightNearActionGrabEntity = new NearActionGrabEntity(RIGHT_HAND);
  
      enableDispatcherModule("LeftNearActionGrabEntity", leftNearActionGrabEntity);
      enableDispatcherModule("RightNearActionGrabEntity", rightNearActionGrabEntity);
  
      function cleanup() {
          leftNearActionGrabEntity.cleanup();
          rightNearActionGrabEntity.cleanup();
          disableDispatcherModule("LeftNearActionGrabEntity");
          disableDispatcherModule("RightNearActionGrabEntity");
      }
      Script.scriptEnding.connect(cleanup);
  }());
  