//
//  fingerPaint.js
//
//  Created by David Rowe on 15 Feb 2017
//  Copyright 2017 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
//

(function () {
    var tablet,
        button,
        BUTTON_NAME = "PAINT",
        isFingerPainting = false,
        shouldPointFingers = false,
        leftHand = null,
        rightHand = null,
        leftBrush = null,
        rightBrush = null,
        CONTROLLER_MAPPING_NAME = "com.highfidelity.fingerPaint",
        isTabletDisplayed = false,
        HIFI_POINT_INDEX_MESSAGE_CHANNEL = "Hifi-Point-Index",
        HIFI_GRAB_DISABLE_MESSAGE_CHANNEL = "Hifi-Grab-Disable",
        HIFI_POINTER_DISABLE_MESSAGE_CHANNEL = "Hifi-Pointer-Disable";
        HOW_TO_EXIT_MESSAGE = "Press B on your controller to exit FingerPainting mode";

    function paintBrush(name) {
        // Paints in 3D.
        var brushName = name,
            STROKE_COLOR = { red: 250, green: 0, blue: 0 },
            ERASE_SEARCH_RADIUS = 0.1,  // m
            isDrawingLine = false,
            entityID,
            basePosition,
            strokePoints,
            strokeNormals,
            strokeWidths,
            timeOfLastPoint,
            MIN_STROKE_LENGTH = 0.005,  // m
            MIN_STROKE_INTERVAL = 66,  // ms
            MAX_POINTS_PER_LINE = 70;  // Hard-coded limit in PolyLineEntityItem.h.

        function strokeNormal() {
            return Vec3.multiplyQbyV(Camera.getOrientation(), Vec3.UNIT_NEG_Z);
        }

        function startLine(position, width) {
            // Start drawing a polyline.

            if (isDrawingLine) {
                print("ERROR: startLine() called when already drawing line");
                // Nevertheless, continue on and start a new line.
            }

            basePosition = position;

            strokePoints = [Vec3.ZERO];
            strokeNormals = [strokeNormal()];
            strokeWidths = [width];
            timeOfLastPoint = Date.now();

            entityID = Entities.addEntity({
                type: "PolyLine",
                name: "fingerPainting",
                color: STROKE_COLOR,
                position: position,
                linePoints: strokePoints,
                normals: strokeNormals,
                strokeWidths: strokeWidths,
                dimensions: { x: 10, y: 10, z: 10 }
            });

            isDrawingLine = true;
        }

        function drawLine(position, width) {
            // Add a stroke to the polyline if stroke is a sufficient length.
            var localPosition,
                distanceToPrevious,
                MAX_DISTANCE_TO_PREVIOUS = 1.0;

            if (!isDrawingLine) {
                print("ERROR: drawLine() called when not drawing line");
                return;
            }

            localPosition = Vec3.subtract(position, basePosition);
            distanceToPrevious = Vec3.distance(localPosition, strokePoints[strokePoints.length - 1]);

            if (distanceToPrevious > MAX_DISTANCE_TO_PREVIOUS) {
                // Ignore occasional spurious finger tip positions.
                return;
            }

            if (distanceToPrevious >= MIN_STROKE_LENGTH
                    && (Date.now() - timeOfLastPoint) >= MIN_STROKE_INTERVAL
                    && strokePoints.length < MAX_POINTS_PER_LINE) {
                strokePoints.push(localPosition);
                strokeNormals.push(strokeNormal());
                strokeWidths.push(width);
                timeOfLastPoint = Date.now();

                Entities.editEntity(entityID, {
                    linePoints: strokePoints,
                    normals: strokeNormals,
                    strokeWidths: strokeWidths
                });
            }
        }

        function finishLine(position, width) {
            // Finish drawing polyline; delete if it has only 1 point.

            if (!isDrawingLine) {
                print("ERROR: finishLine() called when not drawing line");
                return;
            }

            if (strokePoints.length === 1) {
                // Delete "empty" line.
                Entities.deleteEntity(entityID);
            }

            isDrawingLine = false;
        }

        function cancelLine() {
            // Cancel any line being drawn.
            if (isDrawingLine) {
                Entities.deleteEntity(entityID);
                isDrawingLine = false;
            }
        }

        function eraseClosestLine(position) {
            // Erase closest line that is within search radius of finger tip.
            var entities,
                entitiesLength,
                properties,
                i,
                pointsLength,
                j,
                distance,
                found = false,
                foundID,
                foundDistance = ERASE_SEARCH_RADIUS;

            // Find entities with bounding box within search radius.
            entities = Entities.findEntities(position, ERASE_SEARCH_RADIUS);

            // Fine polyline entity with closest point within search radius.
            for (i = 0, entitiesLength = entities.length; i < entitiesLength; i += 1) {
                properties = Entities.getEntityProperties(entities[i], ["type", "position", "linePoints"]);
                if (properties.type === "PolyLine") {
                    basePosition = properties.position;
                    for (j = 0, pointsLength = properties.linePoints.length; j < pointsLength; j += 1) {
                        distance = Vec3.distance(position, Vec3.sum(basePosition, properties.linePoints[j]));
                        if (distance <= foundDistance) {
                            found = true;
                            foundID = entities[i];
                            foundDistance = distance;
                        }
                    }
                }
            }

            // Delete found entity.
            if (found) {
                Entities.deleteEntity(foundID);
            }
        }

        function tearDown() {
            cancelLine();
        }

        return {
            startLine: startLine,
            drawLine: drawLine,
            finishLine: finishLine,
            cancelLine: cancelLine,
            eraseClosestLine: eraseClosestLine,
            tearDown: tearDown
        };
    }

    function handController(name) {
        // Translates controller data into application events.
        var handName = name,

            triggerPressedCallback,
            triggerPressingCallback,
            triggerReleasedCallback,
            gripPressedCallback,

            rawTriggerValue = 0.0,
            triggerValue = 0.0,
            isTriggerPressed = false,
            TRIGGER_SMOOTH_RATIO = 0.1,
            TRIGGER_OFF = 0.05,
            TRIGGER_ON = 0.1,
            TRIGGER_START_WIDTH_RAMP = 0.15,
            TRIGGER_FINISH_WIDTH_RAMP = 1.0,
            TRIGGER_RAMP_WIDTH = TRIGGER_FINISH_WIDTH_RAMP - TRIGGER_START_WIDTH_RAMP,
            MIN_LINE_WIDTH = 0.005,
            MAX_LINE_WIDTH = 0.03,
            RAMP_LINE_WIDTH = MAX_LINE_WIDTH - MIN_LINE_WIDTH,

            rawGripValue = 0.0,
            gripValue = 0.0,
            isGripPressed = false,
            GRIP_SMOOTH_RATIO = 0.1,
            GRIP_OFF = 0.05,
            GRIP_ON = 0.1;

        function onTriggerPress(value) {
            // Controller values are only updated when they change so store latest for use in update.
            rawTriggerValue = value;
        }

        function updateTriggerPress(value) {
            var wasTriggerPressed,
                fingerTipPosition,
                lineWidth;

            triggerValue = triggerValue * TRIGGER_SMOOTH_RATIO + rawTriggerValue * (1.0 - TRIGGER_SMOOTH_RATIO);

            wasTriggerPressed = isTriggerPressed;
            if (isTriggerPressed) {
                isTriggerPressed = triggerValue > TRIGGER_OFF;
            } else {
                isTriggerPressed = triggerValue > TRIGGER_ON;
            }

            if (wasTriggerPressed || isTriggerPressed) {
                fingerTipPosition = MyAvatar.getJointPosition(handName === "left" ? "LeftHandIndex4" : "RightHandIndex4");
                if (triggerValue < TRIGGER_START_WIDTH_RAMP) {
                    lineWidth = MIN_LINE_WIDTH;
                } else {
                    lineWidth = MIN_LINE_WIDTH
                        + (triggerValue - TRIGGER_START_WIDTH_RAMP) / TRIGGER_RAMP_WIDTH * RAMP_LINE_WIDTH;
                }

                if (!wasTriggerPressed && isTriggerPressed) {
                    triggerPressedCallback(fingerTipPosition, lineWidth);
                } else if (wasTriggerPressed && isTriggerPressed) {
                    triggerPressingCallback(fingerTipPosition, lineWidth);
                } else {
                    triggerReleasedCallback(fingerTipPosition, lineWidth);
                }
            }
        }

        function onGripPress(value) {
            // Controller values are only updated when they change so store latest for use in update.
            rawGripValue = value;
        }

        function updateGripPress() {
            var fingerTipPosition;

            gripValue = gripValue * GRIP_SMOOTH_RATIO + rawGripValue * (1.0 - GRIP_SMOOTH_RATIO);

            if (isGripPressed) {
                isGripPressed = gripValue > GRIP_OFF;
            } else {
                isGripPressed = gripValue > GRIP_ON;
                if (isGripPressed) {
                    fingerTipPosition = MyAvatar.getJointPosition(handName === "left" ? "LeftHandIndex4" : "RightHandIndex4");
                    gripPressedCallback(fingerTipPosition);
                }
            }
        }

        function onUpdate() {
            updateTriggerPress();
            updateGripPress();
        }

        function setUp(onTriggerPressed, onTriggerPressing, onTriggerReleased, onGripPressed) {
            triggerPressedCallback = onTriggerPressed;
            triggerPressingCallback = onTriggerPressing;
            triggerReleasedCallback = onTriggerReleased;
            gripPressedCallback = onGripPressed;
        }

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

        return {
            onTriggerPress: onTriggerPress,
            onGripPress: onGripPress,
            onUpdate: onUpdate,
            setUp: setUp,
            tearDown: tearDown
        };
    }

    function updateHandFunctions() {
        // Update other scripts' hand functions.
        var enabled = !isFingerPainting || isTabletDisplayed;

        Messages.sendMessage(HIFI_GRAB_DISABLE_MESSAGE_CHANNEL, JSON.stringify({
            holdEnabled: enabled,
            nearGrabEnabled: enabled,
            farGrabEnabled: enabled
        }), true);
        Messages.sendMessage(HIFI_POINTER_DISABLE_MESSAGE_CHANNEL, JSON.stringify({
            pointerEnabled: enabled
        }), true);

        var newShouldPointFingers = !enabled;
        if (newShouldPointFingers !== shouldPointFingers) {
            Messages.sendMessage(HIFI_POINT_INDEX_MESSAGE_CHANNEL, JSON.stringify({
                pointIndex: newShouldPointFingers
            }), true);
            shouldPointFingers = newShouldPointFingers;
        }
    }

    function howToExitTutorial() {
        HMD.requestShowHandControllers();
        setControllerPartLayer('button_b', 'highlight');
        messageWindow = Window.alert(HOW_TO_EXIT_MESSAGE);
        setControllerPartLayer('button_b', 'blank');
        HMD.requestHideHandControllers();
        Settings.setValue("FingerPaintTutorialComplete", true);
    }

    function enableProcessing() {
        // Connect controller API to handController objects.
        leftHand = handController("left");
        rightHand = handController("right");
        var controllerMapping = Controller.newMapping(CONTROLLER_MAPPING_NAME);
        controllerMapping.from(Controller.Standard.LT).to(leftHand.onTriggerPress);
        controllerMapping.from(Controller.Standard.LeftGrip).to(leftHand.onGripPress);
        controllerMapping.from(Controller.Standard.RT).to(rightHand.onTriggerPress);
        controllerMapping.from(Controller.Standard.RightGrip).to(rightHand.onGripPress);
        controllerMapping.from(Controller.Standard.B).to(onButtonClicked);
        Controller.enableMapping(CONTROLLER_MAPPING_NAME);
        
        if (!Settings.getValue("FingerPaintTutorialComplete")) {
            howToExitTutorial();
        }

        // Connect handController outputs to paintBrush objects.
        leftBrush = paintBrush("left");
        leftHand.setUp(leftBrush.startLine, leftBrush.drawLine, leftBrush.finishLine, leftBrush.eraseClosestLine);
        rightBrush = paintBrush("right");
        rightHand.setUp(rightBrush.startLine, rightBrush.drawLine, rightBrush.finishLine, rightBrush.eraseClosestLine);

        // Messages channels for enabling/disabling other scripts' functions.
        Messages.subscribe(HIFI_POINT_INDEX_MESSAGE_CHANNEL);
        Messages.subscribe(HIFI_GRAB_DISABLE_MESSAGE_CHANNEL);
        Messages.subscribe(HIFI_POINTER_DISABLE_MESSAGE_CHANNEL);

        // Update hand controls.
        Script.update.connect(leftHand.onUpdate);
        Script.update.connect(rightHand.onUpdate);
    }

    function disableProcessing() {
        Script.update.disconnect(leftHand.onUpdate);
        Script.update.disconnect(rightHand.onUpdate);

        Controller.disableMapping(CONTROLLER_MAPPING_NAME);

        leftBrush.tearDown();
        leftBrush = null;
        leftHand.tearDown();
        leftHand = null;

        rightBrush.tearDown();
        rightBrush = null;
        rightHand.tearDown();
        rightHand = null;

        Messages.unsubscribe(HIFI_POINT_INDEX_MESSAGE_CHANNEL);
        Messages.unsubscribe(HIFI_GRAB_DISABLE_MESSAGE_CHANNEL);
        Messages.unsubscribe(HIFI_POINTER_DISABLE_MESSAGE_CHANNEL);
    }

    function onButtonClicked() {
        var wasFingerPainting = isFingerPainting;

        isFingerPainting = !isFingerPainting;
        button.editProperties({ isActive: isFingerPainting });

        print("Finger painting: " + isFingerPainting ? "on" : "off");

        if (wasFingerPainting) {
            leftBrush.cancelLine();
            rightBrush.cancelLine();
        }

        if (isFingerPainting) {
            enableProcessing();
        }

        updateHandFunctions();

        if (!isFingerPainting) {
            disableProcessing();
        }
    }

    function onTabletScreenChanged(type, url) {
        var TABLET_SCREEN_CLOSED = "Closed";

        isTabletDisplayed = type !== TABLET_SCREEN_CLOSED;
        updateHandFunctions();
    }

    function setUp() {
        tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system");
        if (!tablet) {
            return;
        }

        // Tablet button.
        button = tablet.addButton({
            icon: "icons/tablet-icons/finger-paint-i.svg",
            activeIcon: "icons/tablet-icons/finger-paint-a.svg",
            text: BUTTON_NAME,
            isActive: isFingerPainting
        });
        button.clicked.connect(onButtonClicked);

        // Track whether tablet is displayed or not.
        tablet.screenChanged.connect(onTabletScreenChanged);
    }

    function tearDown() {
        if (!tablet) {
            return;
        }

        if (isFingerPainting) {
            isFingerPainting = false;
            updateHandFunctions();
            disableProcessing();
        }

        tablet.screenChanged.disconnect(onTabletScreenChanged);

        button.clicked.disconnect(onButtonClicked);
        tablet.removeButton(button);
    }
    
    /**
     * A controller is made up of parts, and each part can have multiple "layers,"
     * which are really just different texures. For example, the "trigger" part
     * has "normal" and "highlight" layers.
     */
    function setControllerPartLayer(part, layer) {
        data = {};
        data[part] = layer;
        Messages.sendLocalMessage('Controller-Set-Part-Layer', JSON.stringify(data));
    }

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