//
//  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 */

(function () {

    "use strict";

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

    var // Base overlay
        proxyOverlay = null,
        PROXY_MODEL = Script.resolvePath("./assets/models/tinyTablet.fbx"),
        PROXY_DIMENSIONS = { x: 0.0637, y: 0.0965, z: 0.0046 }, // Proportional to tablet proper.
        PROXY_POSITION_LEFT_HAND = {
            x: 0,
            y: 0.07, // Distance from joint.
            z: 0.07 // Distance above palm.
        },
        PROXY_POSITION_RIGHT_HAND = {
            x: 0,
            y: 0.07, // Distance from joint.
            z: 0.07 // Distance above palm.
        },
        /*
        // Aligned cross-palm.
        PROXY_ROTATION_LEFT_HAND = Quat.fromVec3Degrees({ x: 0, y: 180, z: 90 }),
        PROXY_ROTATION_RIGHT_HAND = Quat.fromVec3Degrees({ x: 0, y: 180, z: -90 }),
        */
        // Aligned with palm.
        PROXY_ROTATION_LEFT_HAND = Quat.fromVec3Degrees({ x: 0, y: 180, z: 0 }),
        PROXY_ROTATION_RIGHT_HAND = Quat.fromVec3Degrees({ x: 0, y: 180, z: 0 }),

        // UI overlay.
        proxyUIOverlay = null,
        PROXY_UI_HTML = Script.resolvePath("./html/miniTablet.html"),
        PROXY_UI_DIMENSIONS = { x: 0.0577, y: 0.0905 },
        PROXY_UI_WIDTH_PIXELS = 150,
        METERS_TO_INCHES = 39.3701,
        PROXY_UI_DPI = PROXY_UI_WIDTH_PIXELS / (PROXY_UI_DIMENSIONS.x * METERS_TO_INCHES),
        PROXY_UI_OFFSET = 0.001, // Above model surface.
        PROXY_UI_LOCAL_POSITION = { x: 0, y: 0, z: -(PROXY_DIMENSIONS.z / 2 + PROXY_UI_OFFSET) },
        PROXY_UI_LOCAL_ROTATION = Quat.fromVec3Degrees({ x: 0, y: 180, z: 0 }),
        proxyUIOverlayEnabled = false,
        PROXY_UI_OVERLAY_ENABLED_DELAY = 500,
        proxyOverlayObject = null,

        MUTE_ON_ICON = Script.resourcesPath() + "icons/tablet-icons/mic-mute-a.svg",
        MUTE_OFF_ICON = Script.resourcesPath() + "icons/tablet-icons/mic-unmute-i.svg",
        BUBBLE_ON_ICON = Script.resourcesPath() + "icons/tablet-icons/bubble-a.svg",
        BUBBLE_OFF_ICON = Script.resourcesPath() + "icons/tablet-icons/bubble-i.svg",

        // State machine
        PROXY_DISABLED = 0,
        PROXY_HIDDEN = 1,
        PROXY_HIDING = 2,
        PROXY_SHOWING = 3,
        PROXY_VISIBLE = 4,
        PROXY_EXPANDING = 5,
        TABLET_OPEN = 6,
        STATE_STRINGS = ["PROXY_DISABLED", "PROXY_HIDDEN", "PROXY_HIDING", "PROXY_SHOWING", "PROXY_VISIBLE", "PROXY_EXPANDING",
            "TABLET_OPEN"],
        STATE_MACHINE,
        rezzerState = PROXY_DISABLED,
        proxyHand,
        PROXY_SCALE_DURATION = 150,
        PROXY_SCALE_TIMEOUT = 20,
        proxyScaleTimer = null,
        proxyScaleStart,
        PROXY_EXPAND_HANDLES = [ // Normalized coordinates in range [-0.5, 0.5] about center of mini tablet.
            { x: 0.5, y: -0.75, z: 0 },
            { x: -0.5, y: -0.75, z: 0 }
        ],
        PROXY_EXPAND_DELTA_ROTATION = Quat.fromVec3Degrees({ x: -45, y: 0, z: 0 }),
        proxyExpandHand,
        proxyExpandLocalPosition,
        proxyExpandLocalRotation = Quat.IDENTITY,
        PROXY_EXPAND_DURATION = 250,
        PROXY_EXPAND_TIMEOUT = 20,
        proxyExpandTimer = null,
        proxyExpandStart,
        proxyInitialWidth,
        proxyTargetWidth,
        proxyTargetLocalRotation,

        // EventBridge
        READY_MESSAGE = "ready", // Engine <== Dialog
        HOVER_MESSAGE = "hover", // Engine <== Dialog
        MUTE_MESSAGE = "mute", // Engine <=> Dialog
        BUBBLE_MESSAGE = "bubble", // Engine <=> Dialog
        EXPAND_MESSAGE = "expand", // Engine <== Dialog

        // Events
        MIN_HAND_CAMERA_ANGLE = 30,
        DEGREES_180 = 180,
        MIN_HAND_CAMERA_ANGLE_COS = Math.cos(Math.PI * MIN_HAND_CAMERA_ANGLE / DEGREES_180),
        updateTimer = null,
        UPDATE_INTERVAL = 300,
        HIFI_OBJECT_MANIPULATION_CHANNEL = "Hifi-Object-Manipulation",
        avatarScale = 1,

        // 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)),

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

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

    // #region Utilities =======================================================================================================

    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;
    }

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

    // #endregion

    // #region Communications ==================================================================================================

    function updateMiniTabletID() {
        // Send mini-tablet overlay ID to controllerDispatcher so that it can use a smaller near grab distance.
        Messages.sendLocalMessage("Hifi-MiniTablet-ID", proxyOverlay);
        // Send mini-tablet UI overlay ID to stylusInput so that it styluses can be used on it.
        Messages.sendLocalMessage("Hifi-MiniTablet-UI-ID", proxyUIOverlay);
    }

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

    function updateBubbleStatus() {
        var isBubbleOn = Users.getIgnoreRadiusEnabled();
        proxyOverlayObject.emitScriptEvent(JSON.stringify({
            type: BUBBLE_MESSAGE,
            on: isBubbleOn,
            icon: isBubbleOn ? BUBBLE_ON_ICON : BUBBLE_OFF_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();
                updateBubbleStatus();
                break;
            case HOVER_MESSAGE:
                // Audio feedback.
                playSound(hoverSound, HOVER_VOLUME);
                break;
            case MUTE_MESSAGE:
                // Toggle mute.
                playSound(clickSound, CLICK_VOLUME);
                Audio.muted = !Audio.muted;
                break;
            case BUBBLE_MESSAGE:
                // Toggle bubble.
                playSound(clickSound, CLICK_VOLUME);
                Users.toggleIgnoreRadius();
                break;
            case EXPAND_MESSAGE:
                // Expand tablet;
                playSound(clickSound, CLICK_VOLUME);
                setState(PROXY_EXPANDING, proxyHand);
                break;
        }
    }

    // #endregion

    // #region UI ==============================================================================================================

    function createUI() {
        proxyOverlay = Overlays.addOverlay("model", {
            url: PROXY_MODEL,
            dimensions: Vec3.multiply(avatarScale, PROXY_DIMENSIONS),
            solid: true,
            grabbable: true,
            showKeyboardFocusHighlight: false,
            displayInFront: true,
            visible: false
        });
        proxyUIOverlay = Overlays.addOverlay("web3d", {
            url: PROXY_UI_HTML,
            parentID: proxyOverlay,
            localPosition: Vec3.multiply(avatarScale, PROXY_UI_LOCAL_POSITION),
            localRotation: PROXY_UI_LOCAL_ROTATION,
            dimensions: Vec3.multiply(avatarScale, PROXY_UI_DIMENSIONS),
            dpi: PROXY_UI_DPI / avatarScale,
            alpha: 0, // Hide overlay while its content is being created.
            grabbable: false,
            showKeyboardFocusHighlight: false,
            displayInFront: true,
            visible: false
        });

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

        proxyOverlayObject = Overlays.getOverlayObject(proxyUIOverlay);
        proxyOverlayObject.webEventReceived.connect(onWebEventReceived);

        // updateMiniTabletID(); Other scripts relying on this may not be ready yet so do this in showUI().
    }

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

        Overlays.editOverlay(proxyOverlay, {
            parentID: MyAvatar.SELF_ID,
            parentJointIndex: handJointIndex(proxyHand),
            localPosition: Vec3.multiply(avatarScale,
                proxyHand === LEFT_HAND ? PROXY_POSITION_LEFT_HAND : PROXY_POSITION_RIGHT_HAND),
            localRotation: proxyHand === LEFT_HAND ? PROXY_ROTATION_LEFT_HAND : PROXY_ROTATION_RIGHT_HAND,
            dimensions: Vec3.multiply(initialScale, PROXY_DIMENSIONS),
            visible: true
        });
        Overlays.editOverlay(proxyUIOverlay, {
            localPosition: Vec3.multiply(avatarScale, PROXY_UI_LOCAL_POSITION),
            localRotation: PROXY_UI_LOCAL_ROTATION,
            dimensions: Vec3.multiply(initialScale, PROXY_UI_DIMENSIONS),
            dpi: PROXY_UI_DPI / initialScale,
            visible: true
        });

        updateMiniTabletID();

        if (!proxyUIOverlayEnabled) {
            // 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(proxyUIOverlay, { alpha: 1.0 });
            }, PROXY_UI_OVERLAY_ENABLED_DELAY);
        }
    }

    function sizeUI(scaleFactor) {
        // Scale UI in place.
        Overlays.editOverlay(proxyOverlay, {
            dimensions: Vec3.multiply(scaleFactor, PROXY_DIMENSIONS)
        });
        Overlays.editOverlay(proxyUIOverlay, {
            localPosition: Vec3.multiply(scaleFactor, PROXY_UI_LOCAL_POSITION),
            dimensions: Vec3.multiply(scaleFactor, PROXY_UI_DIMENSIONS),
            dpi: PROXY_UI_DPI / scaleFactor
        });
    }

    function sizeUIAboutHandles(scaleFactor) {
        // Scale UI and move per handles.
        var tabletScaleFactor = avatarScale * (1 + scaleFactor * (proxyTargetWidth - proxyInitialWidth) / proxyInitialWidth);
        var dimensions = Vec3.multiply(tabletScaleFactor, PROXY_DIMENSIONS);
        var localRotation = Quat.mix(proxyExpandLocalRotation, proxyTargetLocalRotation, scaleFactor);
        var localPosition =
            Vec3.sum(proxyExpandLocalPosition,
                Vec3.multiplyQbyV(proxyExpandLocalRotation,
                    Vec3.multiply(-tabletScaleFactor,
                        Vec3.multiplyVbyV(PROXY_EXPAND_HANDLES[proxyExpandHand], PROXY_DIMENSIONS)))
            );
        localPosition = Vec3.sum(localPosition,
            Vec3.multiplyQbyV(proxyExpandLocalRotation, { 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(proxyOverlay, {
            localPosition: localPosition,
            localRotation: localRotation,
            dimensions: dimensions
        });
        Overlays.editOverlay(proxyUIOverlay, {
            localPosition: Vec3.multiply(tabletScaleFactor, PROXY_UI_LOCAL_POSITION),
            dimensions: Vec3.multiply(tabletScaleFactor, PROXY_UI_DIMENSIONS),
            dpi: PROXY_UI_DPI / tabletScaleFactor
        });
    }

    function hideUI() {
        Overlays.editOverlay(proxyOverlay, {
            parentID: Uuid.NULL, // Release hold so that hand can grab tablet proper.
            visible: false
        });
        Overlays.editOverlay(proxyUIOverlay, {
            visible: false
        });
    }

    function destroyUI() {
        if (proxyOverlayObject) {
            proxyOverlayObject.webEventReceived.disconnect(onWebEventReceived);
            Overlays.deleteOverlay(proxyUIOverlay);
            Overlays.deleteOverlay(proxyOverlay);
            proxyOverlayObject = null;
            proxyUIOverlay = null;
            proxyOverlay = null;
            updateMiniTabletID();
        }
    }

    // #endregion

    // #region State Machine ===================================================================================================

    function enterProxyDisabled() {
        // Stop updates.
        if (updateTimer !== null) {
            Script.clearTimeout(updateTimer);
            updateTimer = null;
        }

        // Stop monitoring mute and bubble changes.
        Audio.mutedChanged.disconnect(updateMutedStatus);
        Users.ignoreRadiusEnabledChanged.disconnect(updateBubbleStatus);

        // Don't keep overlays prepared if in desktop mode.
        destroyUI();
    }

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

        // Start monitoring mute and bubble changes.
        Audio.mutedChanged.connect(updateMutedStatus);
        Users.ignoreRadiusEnabledChanged.connect(updateBubbleStatus);

        // Start updates.
        updateTimer = Script.setTimeout(updateState, UPDATE_INTERVAL);
    }

    function shouldShowProxy(hand) {
        // Should show tablet proxy if hand is oriented toward the camera and the camera is oriented toward the proxy tablet.
        var pose,
            jointIndex,
            handPosition,
            handOrientation,
            cameraToHandDirection;

        pose = Controller.getPoseValue(hand === LEFT_HAND ? Controller.Standard.LeftHand : Controller.Standard.RightHand);
        if (!pose.valid) {
            return false;
        }

        jointIndex = handJointIndex(hand);
        handPosition = Vec3.sum(MyAvatar.position,
            Vec3.multiplyQbyV(MyAvatar.orientation, MyAvatar.getAbsoluteJointTranslationInObjectFrame(jointIndex)));
        handOrientation = Quat.multiply(MyAvatar.orientation, MyAvatar.getAbsoluteJointRotationInObjectFrame(jointIndex));
        cameraToHandDirection = Vec3.normalize(Vec3.subtract(handPosition, Camera.position));

        return Vec3.dot(cameraToHandDirection, Quat.getForward(handOrientation)) > MIN_HAND_CAMERA_ANGLE_COS;
    }

    function enterProxyHidden() {
        hideUI();
    }

    function updateProxyHidden() {
        // Don't show proxy if tablet is already displayed or in toolbar mode.
        if (HMD.showTablet || tablet.toolbarMode) {
            return;
        }
        // Compare palm directions of hands with vectors from palms to camera.
        if (shouldShowProxy(LEFT_HAND)) {
            setState(PROXY_SHOWING, LEFT_HAND);
        } else if (shouldShowProxy(RIGHT_HAND)) {
            setState(PROXY_SHOWING, RIGHT_HAND);
        }
    }

    function scaleProxyDown() {
        var scaleFactor = (Date.now() - proxyScaleStart) / PROXY_SCALE_DURATION;
        if (scaleFactor < 1) {
            sizeUI((1 - scaleFactor) * avatarScale);
            proxyScaleTimer = Script.setTimeout(scaleProxyDown, PROXY_SCALE_TIMEOUT);
            return;
        }
        proxyScaleTimer = null;
        setState(PROXY_HIDDEN);
    }

    function enterProxyHiding() {
        proxyScaleStart = Date.now();
        proxyScaleTimer = Script.setTimeout(scaleProxyDown, PROXY_SCALE_TIMEOUT);
    }

    function updateProxyHiding() {
        if (HMD.showTablet) {
            setState(PROXY_HIDDEN);
        }
    }

    function exitProxyHiding() {
        if (proxyScaleTimer) {
            Script.clearTimeout(proxyScaleTimer);
            proxyScaleTimer = null;
        }
    }

    function scaleProxyUp() {
        var scaleFactor = (Date.now() - proxyScaleStart) / PROXY_SCALE_DURATION;
        if (scaleFactor < 1) {
            sizeUI(scaleFactor * avatarScale);
            proxyScaleTimer = Script.setTimeout(scaleProxyUp, PROXY_SCALE_TIMEOUT);
            return;
        }
        proxyScaleTimer = null;
        sizeUI(avatarScale);
        setState(PROXY_VISIBLE);
    }

    function enterProxyShowing(hand) {
        proxyHand = hand;
        showUI();
        proxyScaleStart = Date.now();
        proxyScaleTimer = Script.setTimeout(scaleProxyUp, PROXY_SCALE_TIMEOUT);
    }

    function updateProxyShowing() {
        if (HMD.showTablet) {
            setState(PROXY_HIDDEN);
        }
    }

    function exitProxyShowing() {
        if (proxyScaleTimer) {
            Script.clearTimeout(proxyScaleTimer);
            proxyScaleTimer = null;
        }
    }

    function updateProxyVisible() {
        // Hide proxy if tablet has been displayed by other means.
        if (HMD.showTablet) {
            setState(PROXY_HIDDEN);
            return;
        }
        // Check that palm direction of proxy hand still less than maximum angle.
        if (!shouldShowProxy(proxyHand)) {
            setState(PROXY_HIDING);
        }
    }

    function expandProxy() {
        var scaleFactor = (Date.now() - proxyExpandStart) / PROXY_EXPAND_DURATION;
        if (scaleFactor < 1) {
            sizeUIAboutHandles(scaleFactor);
            proxyExpandTimer = Script.setTimeout(expandProxy, PROXY_EXPAND_TIMEOUT);
            return;
        }
        proxyExpandTimer = null;
        setState(TABLET_OPEN);
    }

    function enterProxyExpanding(hand) {
        // Grab details.
        var properties = Overlays.getProperties(proxyOverlay, ["localPosition", "localRotation"]);
        proxyExpandHand = hand;
        proxyExpandLocalRotation = properties.localRotation;
        proxyExpandLocalPosition = Vec3.sum(properties.localPosition,
            Vec3.multiplyQbyV(proxyExpandLocalRotation,
                Vec3.multiplyVbyV(PROXY_EXPAND_HANDLES[proxyExpandHand], PROXY_DIMENSIONS)));

        // Start expanding.
        proxyInitialWidth = PROXY_DIMENSIONS.x;
        proxyTargetWidth = getTabletWidthFromSettings();
        proxyTargetLocalRotation = Quat.multiply(proxyExpandLocalRotation, PROXY_EXPAND_DELTA_ROTATION);
        proxyExpandStart = Date.now();
        proxyExpandTimer = Script.setTimeout(expandProxy, PROXY_EXPAND_TIMEOUT);
    }

    function updateProxyExanding() {
        // Hide proxy immediately if tablet has been displayed by other means.
        if (HMD.showTablet) {
            setState(PROXY_HIDDEN);
        }
    }

    function exitProxyExpanding() {
        if (proxyExpandTimer !== null) {
            Script.clearTimeout(proxyExpandTimer);
            proxyExpandTimer = null;
        }
    }

    function enterTabletOpen() {
        var proxyOverlayProperties = Overlays.getProperties(proxyOverlay, ["position", "orientation"]);

        hideUI();

        Overlays.editOverlay(HMD.tabletID, {
            position: proxyOverlayProperties.position,
            orientation: proxyOverlayProperties.orientation
        });
        HMD.openTablet(true);
    }

    function updateTabletOpen() {
        // Immediately transition back to PROXY_HIDDEN.
        setState(PROXY_HIDDEN);
    }

    STATE_MACHINE = {
        PROXY_DISABLED: { // Tablet proxy cannot be shown because in desktop mode.
            enter: enterProxyDisabled,
            update: null,
            exit: exitProxyDisabled
        },
        PROXY_HIDDEN: { // Tablet proxy could be shown but isn't because hand is oriented to show it or aren't in HMD mode.
            enter: enterProxyHidden,
            update: updateProxyHidden,
            exit: null
        },
        PROXY_HIDING: { // Tablet proxy is reducing from PROXY_VISIBLE to PROXY_HIDDEN.
            enter: enterProxyHiding,
            update: updateProxyHiding,
            exit: exitProxyHiding
        },
        PROXY_SHOWING: { // Tablet proxy is expanding from PROXY_HIDDN to PROXY_VISIBLE.
            enter: enterProxyShowing,
            update: updateProxyShowing,
            exit: exitProxyShowing
        },
        PROXY_VISIBLE: { // Tablet proxy is visible and attached to hand.
            enter: null,
            update: updateProxyVisible,
            exit: null
        },
        PROXY_EXPANDING: { // Tablet proxy has been grabbed and is expanding before showing tablet proper.
            enter: enterProxyExpanding,
            update: updateProxyExanding,
            exit: exitProxyExpanding
        },
        TABLET_OPEN: { // Tablet proper is being displayed.
            enter: enterTabletOpen,
            update: updateTabletOpen,
            exit: null
        }
    };

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

    function updateState() {
        if (STATE_MACHINE[STATE_STRINGS[rezzerState]].update) {
            STATE_MACHINE[STATE_STRINGS[rezzerState]].update();
        }
        updateTimer = Script.setTimeout(updateState, UPDATE_INTERVAL);
    }

    // #endregion

    // #region Events ==========================================================================================================

    function onScaleChanged() {
        avatarScale = MyAvatar.scale;
        // Clamp scale in order to work around M17434.
        avatarScale = Math.max(MyAvatar.getDomainMinScale(), Math.min(MyAvatar.getDomainMaxScale(), avatarScale));
    }

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

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

        message = JSON.parse(data);
        if (message.grabbedEntity !== proxyOverlay) {
            return;
        }

        if (message.action === "grab" && rezzerState === PROXY_VISIBLE) {
            hand = message.joint === HAND_NAMES[proxyHand] ? proxyHand : otherHand(proxyHand);
            setState(PROXY_EXPANDING, hand);
        }
    }

    function onDisplayModeChanged() {
        // Tablet proxy only available when HMD is active.
        if (HMD.active) {
            setState(PROXY_HIDDEN);
        } else {
            setState(PROXY_DISABLED);
        }
    }

    // #endregion

    // #region Start-up and tear-down ==========================================================================================

    function setUp() {
        MyAvatar.scaleChanged.connect(onScaleChanged);

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

        HMD.displayModeChanged.connect(onDisplayModeChanged);
        if (HMD.active) {
            setState(PROXY_HIDDEN);
        }
    }

    function tearDown() {
        if (updateTimer !== null) {
            Script.clearTimeout(updateTimer);
            updateTimer = null;
        }

        setState(PROXY_DISABLED);

        HMD.displayModeChanged.disconnect(onDisplayModeChanged);

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

        MyAvatar.scaleChanged.disconnect(onScaleChanged);
    }

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

    // #endregion

}());