"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);
}());