//
//  miniTablet.js
//
//  Created by David Rowe on 9 Aug 2018.
//  Copyright 2018 High Fidelity, Inc.
//
//  Distributed under the Apache License, Version 2.0.
//  See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//

/* global getTabletWidthFromSettings, TRIGGER_OFF_VALUE */

(function () {

    "use strict";

    Script.include("./libraries/utils.js");
    Script.include("./libraries/controllerDispatcherUtils.js");

    var UI,
        ui = null,
        State,
        miniState = null,

        // Hands.
        NO_HAND = -1,
        LEFT_HAND = 0,
        RIGHT_HAND = 1,
        HAND_NAMES = ["LeftHand", "RightHand"],

        // Track controller grabbing state.
        HIFI_OBJECT_MANIPULATION_CHANNEL = "Hifi-Object-Manipulation",
        grabbingHand = NO_HAND,
        grabbedItem = null,

        // Miscellaneous.
        tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"),
        DEBUG = false;


    function debug(message) {
        if (!DEBUG) {
            return;
        }
        print("DEBUG: " + message);
    }

    function error(message) {
        print("ERROR: " + message);
    }

    function handJointName(hand) {
        var jointName;
        if (hand === LEFT_HAND) {
            if (Camera.mode === "first person") {
                jointName = "_CONTROLLER_LEFTHAND";
            } else if (Camera.mode === "third person") {
                jointName = "_CAMERA_RELATIVE_CONTROLLER_LEFTHAND";
            } else {
                jointName = "LeftHand";
            }
        } else {
            if (Camera.mode === "first person") {
                jointName = "_CONTROLLER_RIGHTHAND";
            } else if (Camera.mode === "third person") {
                jointName = "_CAMERA_RELATIVE_CONTROLLER_RIGHTHAND";
            } else {
                jointName = "RightHand";
            }
        }
        return jointName;
    }

    function handJointIndex(hand) {
        return MyAvatar.getJointIndex(handJointName(hand));
    }

    function otherHand(hand) {
        return hand === LEFT_HAND ? RIGHT_HAND : LEFT_HAND;
    }


    UI = function () {

        if (!(this instanceof UI)) {
            return new UI();
        }

        var // Base overlay
            miniOverlay = null,
            MINI_MODEL = Script.resolvePath("./assets/models/miniTabletBlank.fbx"),
            MINI_DIMENSIONS = { x: 0.0637, y: 0.0965, z: 0.0045 }, // Proportional to tablet proper.
            MINI_POSITIONS = [
                {
                    x: -0.01, // Distance across hand.
                    y: 0.08, // Distance from joint.
                    z: 0.06 // Distance above palm.
                },
                {
                    x: 0.01, // Distance across hand.
                    y: 0.08, // Distance from joint.
                    z: 0.06 // Distance above palm.
                }
            ],
            DEGREES_180 = 180,
            MINI_PICTH = 40,
            MINI_ROTATIONS = [
                Quat.fromVec3Degrees({ x: 0, y: DEGREES_180 - MINI_PICTH, z: 90 }),
                Quat.fromVec3Degrees({ x: 0, y: DEGREES_180 + MINI_PICTH, z: -90 })
            ],

            // UI overlay.
            uiHand = LEFT_HAND,
            miniUIOverlay = null,
            MINI_UI_HTML = Script.resolvePath("./html/miniTablet.html"),
            MINI_UI_DIMENSIONS = { x: 0.059, y: 0.0865 },
            MINI_UI_WIDTH_PIXELS = 150,
            METERS_TO_INCHES = 39.3701,
            MINI_UI_DPI = MINI_UI_WIDTH_PIXELS / (MINI_UI_DIMENSIONS.x * METERS_TO_INCHES),
            MINI_UI_OFFSET = 0.001, // Above model surface.
            MINI_UI_LOCAL_POSITION = { x: 0.0002, y: 0.0024, z: -(MINI_DIMENSIONS.z / 2 + MINI_UI_OFFSET) },
            MINI_UI_LOCAL_ROTATION = Quat.fromVec3Degrees({ x: 0, y: 180, z: 0 }),
            miniUIOverlayEnabled = false,
            MINI_UI_OVERLAY_ENABLED_DELAY = 500,
            miniOverlayObject = null,

            // Button icons.
            MUTE_ON_ICON = Script.resourcesPath() + "icons/tablet-icons/mic-mute-a.svg",
            MUTE_OFF_ICON = Script.resourcesPath() + "icons/tablet-icons/mic-unmute-i.svg",
            GOTO_ICON = Script.resourcesPath() + "icons/tablet-icons/goto-i.svg",

            // Expansion to tablet.
            MINI_EXPAND_HANDLES = [ // Normalized coordinates in range [-0.5, 0.5] about center of mini tablet.
                { x: 0.5, y: -0.65, z: 0 },
                { x: -0.5, y: -0.65, z: 0 }
            ],
            MINI_EXPAND_DELTA_ROTATION = Quat.fromVec3Degrees({ x: -5, y: 0, z: 0 }),
            MINI_EXPAND_HANDLES_OTHER = [ // Different handles when expanding after being grabbed by other hand,
                { x: 0.5, y: -0.4, z: 0 },
                { x: -0.5, y: -0.4, z: 0 }
            ],
            MINI_EXPAND_DELTA_ROTATION_OTHER = Quat.IDENTITY,
            miniExpandHand,
            miniExpandHandles = MINI_EXPAND_HANDLES,
            miniExpandDeltaRotation = MINI_EXPAND_HANDLES_OTHER,
            miniExpandLocalPosition,
            miniExpandLocalRotation = Quat.IDENTITY,
            miniInitialWidth,
            miniTargetWidth,
            miniTargetLocalRotation,

            // Laser pointing at.
            isBodyHovered = false,

            // EventBridge.
            READY_MESSAGE = "ready", // Engine <== Dialog
            HOVER_MESSAGE = "hover", // Engine <== Dialog
            UNHOVER_MESSAGE = "unhover", // Engine <== Dialog
            MUTE_MESSAGE = "mute", // Engine <=> Dialog
            GOTO_MESSAGE = "goto", // Engine <=> Dialog
            EXPAND_MESSAGE = "expand", // Engine <== Dialog

            // Sounds.
            HOVER_SOUND = "./assets/sounds/button-hover.wav",
            HOVER_VOLUME = 0.5,
            CLICK_SOUND = "./assets/sounds/button-click.wav",
            CLICK_VOLUME = 0.8,
            hoverSound = SoundCache.getSound(Script.resolvePath(HOVER_SOUND)),
            clickSound = SoundCache.getSound(Script.resolvePath(CLICK_SOUND));


        function updateMutedStatus() {
            var isMuted = Audio.muted;
            miniOverlayObject.emitScriptEvent(JSON.stringify({
                type: MUTE_MESSAGE,
                on: isMuted,
                icon: isMuted ? MUTE_ON_ICON : MUTE_OFF_ICON
            }));
        }

        function setGotoIcon() {
            miniOverlayObject.emitScriptEvent(JSON.stringify({
                type: GOTO_MESSAGE,
                icon: GOTO_ICON
            }));
        }

        function onWebEventReceived(data) {
            var message;

            try {
                message = JSON.parse(data);
            } catch (e) {
                console.error("EventBridge message error");
                return;
            }

            switch (message.type) {
                case READY_MESSAGE:
                    // Send initial button statuses.
                    updateMutedStatus();
                    setGotoIcon();
                    break;
                case HOVER_MESSAGE:
                    if (message.target === "body") {
                        // Laser status.
                        isBodyHovered = true;
                    } else if (message.target === "button") {
                        // Audio feedback.
                        playSound(hoverSound, HOVER_VOLUME);
                    }
                    break;
                case UNHOVER_MESSAGE:
                    if (message.target === "body") {
                        // Laser status.
                        isBodyHovered = false;
                    }
                    break;
                case MUTE_MESSAGE:
                    // Toggle mute.
                    playSound(clickSound, CLICK_VOLUME);
                    Audio.muted = !Audio.muted;
                    break;
                case GOTO_MESSAGE:
                    // Goto.
                    playSound(clickSound, CLICK_VOLUME);
                    miniState.setState(miniState.MINI_EXPANDING, { hand: uiHand, goto: true });
                    break;
                case EXPAND_MESSAGE:
                    // Expand tablet;
                    playSound(clickSound, CLICK_VOLUME);
                    miniState.setState(miniState.MINI_EXPANDING, { hand: uiHand, goto: false });
                    break;
            }
        }


        function updateMiniTabletID() {
            HMD.miniTabletID = miniOverlay;
            HMD.miniTabletScreenID = miniUIOverlay;
            HMD.miniTabletHand = miniOverlay ? uiHand : -1;
        }

        function playSound(sound, volume) {
            Audio.playSound(sound, {
                position: uiHand === LEFT_HAND ? MyAvatar.getLeftPalmPosition() : MyAvatar.getRightPalmPosition(),
                volume: volume,
                localOnly: true
            });
        }


        function getUIPositionAndRotation(hand) {
            return {
                position: MINI_POSITIONS[hand],
                rotation: MINI_ROTATIONS[hand]
            };
        }

        function getMiniTabletID() {
            return miniOverlay;
        }

        function getMiniTabletProperties() {
            var properties = Overlays.getProperties(miniOverlay, ["position", "orientation"]);
            return {
                position: properties.position,
                orientation: properties.orientation
            };
        }

        function isLaserPointingAt() {
            return isBodyHovered;
        }


        function show(hand) {
            var initialScale = 0.01; // Start very small.

            uiHand = hand;

            Overlays.editOverlay(miniOverlay, {
                parentID: MyAvatar.SELF_ID,
                parentJointIndex: handJointIndex(hand),
                localPosition: Vec3.multiply(MyAvatar.sensorToWorldScale, MINI_POSITIONS[hand]),
                localRotation: MINI_ROTATIONS[hand],
                dimensions: Vec3.multiply(initialScale, MINI_DIMENSIONS),
                grabbable: true,
                visible: true
            });
            Overlays.editOverlay(miniUIOverlay, {
                localPosition: Vec3.multiply(MyAvatar.sensorToWorldScale, MINI_UI_LOCAL_POSITION),
                localRotation: MINI_UI_LOCAL_ROTATION,
                dimensions: Vec3.multiply(initialScale, MINI_UI_DIMENSIONS),
                dpi: MINI_UI_DPI / initialScale,
                visible: true
            });

            updateMiniTabletID();

            if (!miniUIOverlayEnabled) {
                // Overlay content is created the first time it is visible to the user. The initial creation displays artefacts.
                // Delay showing UI overlay until after giving it time for its content to be created.
                Script.setTimeout(function () {
                    Overlays.editOverlay(miniUIOverlay, { alpha: 1.0 });
                }, MINI_UI_OVERLAY_ENABLED_DELAY);
            }
        }

        function size(scaleFactor) {
            // Scale UI in place.
            Overlays.editOverlay(miniOverlay, {
                dimensions: Vec3.multiply(scaleFactor, MINI_DIMENSIONS)
            });
            Overlays.editOverlay(miniUIOverlay, {
                localPosition: Vec3.multiply(scaleFactor, MINI_UI_LOCAL_POSITION),
                dimensions: Vec3.multiply(scaleFactor, MINI_UI_DIMENSIONS),
                dpi: MINI_UI_DPI / scaleFactor
            });
            updateRotation();
        }

        function startExpandingTablet(hand) {
            // Expansion details.
            if (hand === uiHand) {
                miniExpandHandles = MINI_EXPAND_HANDLES;
                miniExpandDeltaRotation = MINI_EXPAND_DELTA_ROTATION;
            } else {
                miniExpandHandles = MINI_EXPAND_HANDLES_OTHER;
                miniExpandDeltaRotation = MINI_EXPAND_DELTA_ROTATION_OTHER;
            }

            // Grab details.
            var properties = Overlays.getProperties(miniOverlay, ["localPosition", "localRotation"]);
            miniExpandHand = hand;
            miniExpandLocalRotation = properties.localRotation;
            miniExpandLocalPosition = Vec3.sum(properties.localPosition,
                Vec3.multiplyQbyV(miniExpandLocalRotation,
                    Vec3.multiplyVbyV(miniExpandHandles[miniExpandHand], MINI_DIMENSIONS)));

            // Start expanding.
            miniInitialWidth = MINI_DIMENSIONS.x;
            miniTargetWidth = getTabletWidthFromSettings();
            miniTargetLocalRotation = Quat.multiply(miniExpandLocalRotation, miniExpandDeltaRotation);
        }

        function sizeAboutHandles(scaleFactor) {
            // Scale UI and move per handles.
            var tabletScaleFactor,
                dimensions,
                localRotation,
                localPosition;

            tabletScaleFactor = MyAvatar.sensorToWorldScale
                * (1 + scaleFactor * (miniTargetWidth - miniInitialWidth) / miniInitialWidth);
            dimensions = Vec3.multiply(tabletScaleFactor, MINI_DIMENSIONS);
            localRotation = Quat.mix(miniExpandLocalRotation, miniTargetLocalRotation, scaleFactor);
            localPosition =
                Vec3.sum(miniExpandLocalPosition,
                    Vec3.multiplyQbyV(miniExpandLocalRotation,
                        Vec3.multiply(-tabletScaleFactor,
                            Vec3.multiplyVbyV(miniExpandHandles[miniExpandHand], MINI_DIMENSIONS)))
                );
            localPosition = Vec3.sum(localPosition,
                Vec3.multiplyQbyV(miniExpandLocalRotation, { x: 0, y: 0.5 * -dimensions.y, z: 0 }));
            localPosition = Vec3.sum(localPosition,
                Vec3.multiplyQbyV(localRotation, { x: 0, y: 0.5 * dimensions.y, z: 0 }));
            Overlays.editOverlay(miniOverlay, {
                localPosition: localPosition,
                localRotation: localRotation,
                dimensions: dimensions
            });
            // FIXME: Temporary code change to try not displaying UI when mini tablet is expanding to become the tablet proper.
            Overlays.editOverlay(miniUIOverlay, {
                /*
                localPosition: Vec3.multiply(tabletScaleFactor, MINI_UI_LOCAL_POSITION),
                dimensions: Vec3.multiply(tabletScaleFactor, MINI_UI_DIMENSIONS),
                dpi: MINI_UI_DPI / tabletScaleFactor
                */
                visible: false
            });
        }

        function updateRotation() {
            // Update the rotation of the tablet about its face normal so that its base is horizontal.
            var COS_5_DEGREES = 0.996,
                RADIANS_TO_DEGREES = DEGREES_180 / Math.PI,
                defaultLocalRotation,
                handOrientation,
                defaultOrientation,
                faceNormal,
                desiredOrientation,
                defaultYAxis,
                desiredYAxis,
                cross,
                dot,
                deltaAngle,
                deltaRotation,
                localRotation;

            if (Overlays.getProperty(miniOverlay, "parentJointIndex") !== handJointIndex(uiHand)) {
                // Overlay has been grabbed by other hand but this script hasn't received notification yet.
                return;
            }

            defaultLocalRotation = MINI_ROTATIONS[uiHand];
            handOrientation =
                Quat.multiply(MyAvatar.orientation, MyAvatar.getAbsoluteJointRotationInObjectFrame(handJointIndex(uiHand)));
            defaultOrientation = Quat.multiply(handOrientation, defaultLocalRotation);
            faceNormal = Vec3.multiplyQbyV(defaultOrientation, Vec3.UNIT_Z);

            if (Math.abs(Vec3.dot(faceNormal, Vec3.UNIT_Y)) > COS_5_DEGREES) {
                // Don't rotate mini tablet if almost flat in the x-z plane.
                return;
            } else {
                // Rotate the tablet so that its base is parallel with the x-z plane.
                desiredOrientation = Quat.lookAt(Vec3.ZERO, Vec3.multiplyQbyV(defaultOrientation, Vec3.UNIT_Z), Vec3.UNIT_Y);
                defaultYAxis = Vec3.multiplyQbyV(defaultOrientation, Vec3.UNIT_Y);
                desiredYAxis = Vec3.multiplyQbyV(desiredOrientation, Vec3.UNIT_Y);
                cross = Vec3.cross(defaultYAxis, desiredYAxis);
                dot = Vec3.dot(defaultYAxis, desiredYAxis);
                deltaAngle = Math.atan2(Vec3.length(cross), dot) * RADIANS_TO_DEGREES;
                if (Vec3.dot(cross, Vec3.multiplyQbyV(desiredOrientation, Vec3.UNIT_Z)) > 0) {
                    deltaAngle = -deltaAngle;
                }
                deltaRotation = Quat.angleAxis(deltaAngle, Vec3.multiplyQbyV(defaultLocalRotation, Vec3.UNIT_Z));
                localRotation = Quat.multiply(deltaRotation, defaultLocalRotation);
            }
            Overlays.editOverlay(miniOverlay, {
                localRotation: localRotation
            });
        }

        function release() {
            Overlays.editOverlay(miniOverlay, {
                parentID: Uuid.NULL, // Release hold so that hand can grab tablet proper.
                grabbable: false
            });
        }

        function hide() {
            Overlays.editOverlay(miniOverlay, {
                visible: false
            });
            Overlays.editOverlay(miniUIOverlay, {
                visible: false
            });
        }

        function create() {
            miniOverlay = Overlays.addOverlay("model", {
                url: MINI_MODEL,
                dimensions: Vec3.multiply(MyAvatar.sensorToWorldScale, MINI_DIMENSIONS),
                solid: true,
                grabbable: true,
                showKeyboardFocusHighlight: false,
                displayInFront: true,
                visible: false
            });
            miniUIOverlay = Overlays.addOverlay("web3d", {
                url: MINI_UI_HTML,
                parentID: miniOverlay,
                localPosition: Vec3.multiply(MyAvatar.sensorToWorldScale, MINI_UI_LOCAL_POSITION),
                localRotation: MINI_UI_LOCAL_ROTATION,
                dimensions: Vec3.multiply(MyAvatar.sensorToWorldScale, MINI_UI_DIMENSIONS),
                dpi: MINI_UI_DPI / MyAvatar.sensorToWorldScale,
                alpha: 0, // Hide overlay while its content is being created.
                grabbable: false,
                showKeyboardFocusHighlight: false,
                displayInFront: true,
                visible: false
            });

            miniUIOverlayEnabled = false; // This and alpha = 0 hides overlay while its content is being created.

            miniOverlayObject = Overlays.getOverlayObject(miniUIOverlay);
            miniOverlayObject.webEventReceived.connect(onWebEventReceived);
        }

        function destroy() {
            if (miniOverlayObject) {
                miniOverlayObject.webEventReceived.disconnect(onWebEventReceived);
                Overlays.deleteOverlay(miniUIOverlay);
                Overlays.deleteOverlay(miniOverlay);
                miniOverlayObject = null;
                miniUIOverlay = null;
                miniOverlay = null;
                updateMiniTabletID();
            }
        }

        create();

        return {
            getUIPositionAndRotation: getUIPositionAndRotation,
            getMiniTabletID: getMiniTabletID,
            getMiniTabletProperties: getMiniTabletProperties,
            isLaserPointingAt: isLaserPointingAt,
            updateMutedStatus: updateMutedStatus,
            show: show,
            size: size,
            startExpandingTablet: startExpandingTablet,
            sizeAboutHandles: sizeAboutHandles,
            updateRotation: updateRotation,
            release: release,
            hide: hide,
            destroy: destroy
        };

    };


    State = function () {

        if (!(this instanceof State)) {
            return new State();
        }

        var
            // States.
            MINI_DISABLED = 0,
            MINI_HIDDEN = 1,
            MINI_HIDING = 2,
            MINI_SHOWING = 3,
            MINI_VISIBLE = 4,
            MINI_EXPANDING = 5,
            TABLET_OPEN = 6,
            STATE_STRINGS = ["MINI_DISABLED", "MINI_HIDDEN", "MINI_HIDING", "MINI_SHOWING", "MINI_VISIBLE", "MINI_EXPANDING",
                "TABLET_OPEN"],
            STATE_MACHINE,
            miniState = MINI_DISABLED,
            miniHand,

            // Mini tablet scaling.
            MINI_SCALE_DURATION = 250,
            MINI_SCALE_TIMEOUT = 20,
            miniScaleTimer = null,
            miniScaleStart,

            // Expansion to tablet.
            MINI_EXPAND_DURATION = 250,
            MINI_EXPAND_TIMEOUT = 20,
            miniExpandTimer = null,
            miniExpandStart,
            miniExpandedStartTime,

            // Tablet targets.
            isGoto = false,
            TABLET_ADDRESS_DIALOG = "hifi/tablet/TabletAddressDialog.qml",

            // Trigger values.
            leftTriggerOn = 0,
            rightTriggerOn = 0,
            MAX_TRIGGER_ON_TIME = 100,

            // Visibility.
            MAX_HAND_CAMERA_ANGLE = 30,
            MAX_CAMERA_HAND_ANGLE = 30,
            DEGREES_180 = 180,
            MAX_HAND_CAMERA_ANGLE_COS = Math.cos(Math.PI * MAX_HAND_CAMERA_ANGLE / DEGREES_180),
            MAX_CAMERA_HAND_ANGLE_COS = Math.cos(Math.PI * MAX_CAMERA_HAND_ANGLE / DEGREES_180),
            HIDING_DELAY = 1000, // ms
            lastVisible = [0, 0];


        function enterMiniDisabled() {
            // Stop updates.
            Script.update.disconnect(updateState);

            // Stop monitoring mute changes.
            Audio.mutedChanged.disconnect(ui.updateMutedStatus);

            // Don't keep overlays prepared if in desktop mode.
            ui.destroy();
            ui = null;
        }

        function exitMiniDisabled() {
            // Create UI so that it's ready to be displayed without seeing artefacts from creating the UI.
            ui = new UI();

            // Start monitoring mute changes.
            Audio.mutedChanged.connect(ui.updateMutedStatus);

            // Start updates.
            Script.update.connect(updateState);
        }

        function shouldShowMini(hand) {
            // Should show mini tablet if it would be oriented toward the camera.
            var show,
                pose,
                isLeftTriggerOff,
                isRightTriggerOff,
                wasLeftTriggerOff = true,
                wasRightTriggerOff = true,
                jointIndex,
                handPosition,
                handOrientation,
                uiPositionAndOrientation,
                miniPosition,
                miniOrientation,
                miniToCameraDirection,
                cameraToHand;

            // Shouldn't show mini tablet if hand isn't being controlled.
            pose = Controller.getPoseValue(hand === LEFT_HAND ? Controller.Standard.LeftHand : Controller.Standard.RightHand);
            show = pose.valid;

            // Shouldn't show mini tablet on hand if that hand's trigger or grip are pressed (i.e., laser is searching or hand 
            // is grabbing something) or the other hand's trigger is pressed unless it is pointing at the mini tablet. Allow 
            // the triggers to be pressed briefly to allow for the grabbing process.
            if (show) {
                isLeftTriggerOff = Controller.getValue(Controller.Standard.LT) < TRIGGER_OFF_VALUE
                    && Controller.getValue(Controller.Standard.LeftGrip) < TRIGGER_OFF_VALUE;
                if (!isLeftTriggerOff) {
                    if (leftTriggerOn === 0) {
                        leftTriggerOn = Date.now();
                    } else {
                        wasLeftTriggerOff = Date.now() - leftTriggerOn < MAX_TRIGGER_ON_TIME;
                    }
                } else {
                    leftTriggerOn = 0;
                }
                isRightTriggerOff = Controller.getValue(Controller.Standard.RT) < TRIGGER_OFF_VALUE
                    && Controller.getValue(Controller.Standard.RightGrip) < TRIGGER_OFF_VALUE;
                if (!isRightTriggerOff) {
                    if (rightTriggerOn === 0) {
                        rightTriggerOn = Date.now();
                    } else {
                        wasRightTriggerOff = Date.now() - rightTriggerOn < MAX_TRIGGER_ON_TIME;
                    }
                } else {
                    rightTriggerOn = 0;
                }

                show = (hand === LEFT_HAND ? wasLeftTriggerOff : wasRightTriggerOff)
                    && ((hand === LEFT_HAND ? wasRightTriggerOff : wasLeftTriggerOff) || ui.isLaserPointingAt());
            }

            // Should show mini tablet if it would be oriented toward the camera.
            if (show) {
                // Calculate current visibility of mini tablet.
                jointIndex = handJointIndex(hand);
                handPosition = Vec3.sum(MyAvatar.position,
                    Vec3.multiplyQbyV(MyAvatar.orientation, MyAvatar.getAbsoluteJointTranslationInObjectFrame(jointIndex)));
                handOrientation =
                    Quat.multiply(MyAvatar.orientation, MyAvatar.getAbsoluteJointRotationInObjectFrame(jointIndex));
                uiPositionAndOrientation = ui.getUIPositionAndRotation(hand);
                miniPosition = Vec3.sum(handPosition, Vec3.multiply(MyAvatar.sensorToWorldScale,
                    Vec3.multiplyQbyV(handOrientation, uiPositionAndOrientation.position)));
                miniOrientation = Quat.multiply(handOrientation, uiPositionAndOrientation.rotation);
                miniToCameraDirection = Vec3.normalize(Vec3.subtract(Camera.position, miniPosition));
                show = Vec3.dot(miniToCameraDirection, Quat.getForward(miniOrientation)) > MAX_HAND_CAMERA_ANGLE_COS;
                show = show || (-Vec3.dot(miniToCameraDirection, Quat.getForward(handOrientation)) > MAX_HAND_CAMERA_ANGLE_COS);
                cameraToHand = -Vec3.dot(miniToCameraDirection, Quat.getForward(Camera.orientation));
                show = show && (cameraToHand > MAX_CAMERA_HAND_ANGLE_COS);

                // Persist showing for a while after it would otherwise be hidden.
                if (show) {
                    lastVisible[hand] = Date.now();
                } else {
                    show = Date.now() - lastVisible[hand] <= HIDING_DELAY;
                }
            }

            return {
                show: show,
                cameraToHand: cameraToHand
            };
        }

        function enterMiniHidden() {
            ui.release();
            ui.hide();
        }

        function updateMiniHidden() {
            var showLeft,
                showRight;

            // Don't show mini tablet if tablet proper is already displayed, in toolbar mode, or away.
            if (HMD.showTablet || tablet.toolbarMode || MyAvatar.isAway) {
                return;
            }

            // Show mini tablet if it would be pointing at the camera.
            showLeft = shouldShowMini(LEFT_HAND);
            showRight = shouldShowMini(RIGHT_HAND);
            if (showLeft.show && showRight.show) {
                // Both hands would be pointing at camera; show the one the camera is gazing at.
                if (showLeft.cameraToHand > showRight.cameraToHand) {
                    setState(MINI_SHOWING, LEFT_HAND);
                } else {
                    setState(MINI_SHOWING, RIGHT_HAND);
                }
            } else if (showLeft.show) {
                setState(MINI_SHOWING, LEFT_HAND);
            } else if (showRight.show) {
                setState(MINI_SHOWING, RIGHT_HAND);
            }
        }

        function scaleMiniDown() {
            var scaleFactor = (Date.now() - miniScaleStart) / MINI_SCALE_DURATION;
            if (scaleFactor < 1) {
                ui.size((1 - scaleFactor) * MyAvatar.sensorToWorldScale);
                miniScaleTimer = Script.setTimeout(scaleMiniDown, MINI_SCALE_TIMEOUT);
                return;
            }
            miniScaleTimer = null;
            setState(MINI_HIDDEN);
        }

        function enterMiniHiding() {
            miniScaleStart = Date.now();
            miniScaleTimer = Script.setTimeout(scaleMiniDown, MINI_SCALE_TIMEOUT);
        }

        function updateMiniHiding() {
            if (HMD.showTablet) {
                setState(MINI_HIDDEN);
            }
        }

        function exitMiniHiding() {
            if (miniScaleTimer) {
                Script.clearTimeout(miniScaleTimer);
                miniScaleTimer = null;
            }
        }

        function scaleMiniUp() {
            var scaleFactor = (Date.now() - miniScaleStart) / MINI_SCALE_DURATION;
            if (scaleFactor < 1) {
                ui.size(scaleFactor * MyAvatar.sensorToWorldScale);
                miniScaleTimer = Script.setTimeout(scaleMiniUp, MINI_SCALE_TIMEOUT);
                return;
            }
            miniScaleTimer = null;
            ui.size(MyAvatar.sensorToWorldScale);
            setState(MINI_VISIBLE);
        }

        function checkMiniVisibility() {
            var showLeft,
                showRight;

            // Check that the mini tablet should still be visible and if so then ensure it's on the hand that the camera is
            // gazing at.
            showLeft = shouldShowMini(LEFT_HAND);
            showRight = shouldShowMini(RIGHT_HAND);
            if (showLeft.show && showRight.show) {
                if (showLeft.cameraToHand > showRight.cameraToHand) {
                    if (miniHand !== LEFT_HAND) {
                        setState(MINI_HIDING);
                    }
                } else {
                    if (miniHand !== RIGHT_HAND) {
                        setState(MINI_HIDING);
                    }
                }
            } else if (showLeft.show) {
                if (miniHand !== LEFT_HAND) {
                    setState(MINI_HIDING);
                }
            } else if (showRight.show) {
                if (miniHand !== RIGHT_HAND) {
                    setState(MINI_HIDING);
                }
            } else {
                if (grabbedItem === null || grabbingHand !== miniHand) {
                    setState(MINI_HIDING);
                } else {
                    setState(MINI_HIDDEN);
                }
            }
        }

        function enterMiniShowing(hand) {
            miniHand = hand;
            ui.show(miniHand);
            miniScaleStart = Date.now();
            miniScaleTimer = Script.setTimeout(scaleMiniUp, MINI_SCALE_TIMEOUT);
        }

        function updateMiniShowing() {
            // Hide mini tablet if tablet proper has been displayed by other means.
            if (HMD.showTablet) {
                setState(MINI_HIDDEN);
            }

            // Hide mini tablet if it should no longer be visible.
            checkMiniVisibility();
        }

        function exitMiniShowing() {
            if (miniScaleTimer) {
                Script.clearTimeout(miniScaleTimer);
                miniScaleTimer = null;
            }
        }

        function updateMiniVisible() {
            // Hide mini tablet if tablet proper has been displayed by other means.
            if (HMD.showTablet) {
                setState(MINI_HIDDEN);
                return;
            }

            // Hide mini tablet if it should no longer be visible.
            checkMiniVisibility();

            // If state hasn't changed, update mini tablet rotation.
            if (miniState === MINI_VISIBLE) {
                ui.updateRotation();
            }
        }

        function expandMini() {
            var scaleFactor = (Date.now() - miniExpandStart) / MINI_EXPAND_DURATION;
            if (scaleFactor < 1) {
                ui.sizeAboutHandles(scaleFactor);
                miniExpandTimer = Script.setTimeout(expandMini, MINI_EXPAND_TIMEOUT);
                return;
            }
            miniExpandTimer = null;
            setState(TABLET_OPEN);
        }

        function enterMiniExpanding(data) {
            isGoto = data.goto;
            ui.startExpandingTablet(data.hand);
            miniExpandStart = Date.now();
            miniExpandTimer = Script.setTimeout(expandMini, MINI_EXPAND_TIMEOUT);
        }

        function updateMiniExanding() {
            // Hide mini tablet immediately if tablet proper has been displayed by other means.
            if (HMD.showTablet) {
                setState(MINI_HIDDEN);
            }
        }

        function exitMiniExpanding() {
            if (miniExpandTimer !== null) {
                Script.clearTimeout(miniExpandTimer);
                miniExpandTimer = null;
            }
        }

        function enterTabletOpen() {
            var miniTabletProperties;

            if (isGoto) {
                tablet.loadQMLSource(TABLET_ADDRESS_DIALOG);
            } else {
                tablet.gotoHomeScreen();
            }

            ui.release();
            miniTabletProperties = ui.getMiniTabletProperties();

            Overlays.editOverlay(HMD.tabletID, {
                position: miniTabletProperties.position,
                orientation: miniTabletProperties.orientation
            });

            HMD.openTablet(true);

            miniExpandedStartTime = Date.now();
        }

        function updateTabletOpen() {
            // Give the tablet proper time to rez before hiding expanded mini tablet.
            // The mini tablet is also hidden elsewhere if the tablet proper is grabbed.
            var TABLET_OPENING_DELAY = 500;
            if (Date.now() >= miniExpandedStartTime + TABLET_OPENING_DELAY) {
                setState(MINI_HIDDEN);
            }
        }

        STATE_MACHINE = {
            MINI_DISABLED: { // Mini tablet cannot be shown because in desktop mode.
                enter: enterMiniDisabled,
                update: null,
                exit: exitMiniDisabled
            },
            MINI_HIDDEN: { // Mini tablet could be shown but isn't because hand is oriented to show it or aren't in HMD mode.
                enter: enterMiniHidden,
                update: updateMiniHidden,
                exit: null
            },
            MINI_HIDING: { // Mini tablet is reducing from MINI_VISIBLE to MINI_HIDDEN.
                enter: enterMiniHiding,
                update: updateMiniHiding,
                exit: exitMiniHiding
            },
            MINI_SHOWING: { // Mini tablet is expanding from MINI_HIDDN to MINI_VISIBLE.
                enter: enterMiniShowing,
                update: updateMiniShowing,
                exit: exitMiniShowing
            },
            MINI_VISIBLE: { // Mini tablet is visible and attached to hand.
                enter: null,
                update: updateMiniVisible,
                exit: null
            },
            MINI_EXPANDING: { // Mini tablet is expanding before showing tablet proper.
                enter: enterMiniExpanding,
                update: updateMiniExanding,
                exit: exitMiniExpanding
            },
            TABLET_OPEN: { // Tablet proper is being displayed.
                enter: enterTabletOpen,
                update: updateTabletOpen,
                exit: null
            }
        };

        function setState(state, data) {
            if (state !== miniState) {
                debug("State transition from " + STATE_STRINGS[miniState] + " to " + STATE_STRINGS[state]
                    + ( data ? " " + JSON.stringify(data) : ""));
                if (STATE_MACHINE[STATE_STRINGS[miniState]].exit) {
                    STATE_MACHINE[STATE_STRINGS[miniState]].exit(data);
                }
                if (STATE_MACHINE[STATE_STRINGS[state]].enter) {
                    STATE_MACHINE[STATE_STRINGS[state]].enter(data);
                }
                miniState = state;
            } else {
                error("Null state transition: " + state + "!");
            }
        }

        function getState() {
            return miniState;
        }

        function getHand() {
            return miniHand;
        }

        function updateState() {
            if (STATE_MACHINE[STATE_STRINGS[miniState]].update) {
                STATE_MACHINE[STATE_STRINGS[miniState]].update();
            }
        }

        function create() {
            // Nothing to do.
        }

        function destroy() {
            if (miniState !== MINI_DISABLED) {
                setState(MINI_DISABLED);
            }
        }

        create();

        return {
            MINI_DISABLED: MINI_DISABLED,
            MINI_HIDDEN: MINI_HIDDEN,
            MINI_HIDING: MINI_HIDING,
            MINI_SHOWING: MINI_SHOWING,
            MINI_VISIBLE: MINI_VISIBLE,
            MINI_EXPANDING: MINI_EXPANDING,
            TABLET_OPEN: TABLET_OPEN,
            setState: setState,
            getState: getState,
            getHand: getHand,
            destroy: destroy
        };
    };


    function onMessageReceived(channel, data, senderID, localOnly) {
        var message,
            miniHand,
            hand;

        if (channel !== HIFI_OBJECT_MANIPULATION_CHANNEL) {
            return;
        }

        try {
            message = JSON.parse(data);
        } catch (e) {
            return;
        }

        // Track grabbed state and item.
        switch (message.action) {
            case "grab":
                grabbingHand = HAND_NAMES.indexOf(message.joint);
                grabbedItem = message.grabbedEntity;
                break;
            case "release":
                grabbingHand = NO_HAND;
                grabbedItem = null;
                break;
            default:
                error("Unexpected grab message!");
                return;
        }

        if (message.grabbedEntity !== HMD.tabletID && message.grabbedEntity !== ui.getMiniTabletID()) {
            return;
        }

        if (message.action === "grab" && message.grabbedEntity === HMD.tabletID && HMD.active) {
            // Tablet may have been grabbed after it replaced expanded mini tablet.
            miniState.setState(miniState.MINI_HIDDEN);
        } else if (message.action === "grab" && miniState.getState() === miniState.MINI_VISIBLE) {
            miniHand = miniState.getHand();
            hand = message.joint === HAND_NAMES[miniHand] ? miniHand : otherHand(miniHand);
            miniState.setState(miniState.MINI_EXPANDING, { hand: hand, goto: false });
        }
    }

    function onWentAway() {
        // Mini tablet only available when user is not away.
        if (HMD.active) {
            miniState.setState(miniState.MINI_HIDDEN);
        }
    }

    function onDisplayModeChanged() {
        // Mini tablet only available when HMD is active.
        if (HMD.active) {
            miniState.setState(miniState.MINI_HIDDEN);
        } else {
            miniState.setState(miniState.MINI_DISABLED);
        }
    }


    function setUp() {
        miniState = new State();

        Messages.subscribe(HIFI_OBJECT_MANIPULATION_CHANNEL);
        Messages.messageReceived.connect(onMessageReceived);

        MyAvatar.wentAway.connect(onWentAway);
        HMD.displayModeChanged.connect(onDisplayModeChanged);
        if (HMD.active) {
            miniState.setState(miniState.MINI_HIDDEN);
        }
    }

    function tearDown() {
        miniState.setState(miniState.MINI_DISABLED);

        HMD.displayModeChanged.disconnect(onDisplayModeChanged);
        MyAvatar.wentAway.disconnect(onWentAway);

        Messages.messageReceived.disconnect(onMessageReceived);
        Messages.unsubscribe(HIFI_OBJECT_MANIPULATION_CHANNEL);

        miniState.destroy();
        miniState = null;
    }

    setUp();
    Script.scriptEnding.connect(tearDown);

}());