//
//  Created by Luis Cuenca on 3/8/19
//  Copyright 2018 High Fidelity, Inc.
//  Copyright 2023 Overte e.V.
//
//
//  Distributed under the Apache License, Version 2.0.
//  See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
/* jslint bitwise: true */
/* global Script, MyAvatar, Tablet
*/

(function () {
    // Adding some additional time before load fix an issue when updating the debugging overlays (local entities)
    var MS_AFTER_LOADING = 500;
    Script.setTimeout(function() {
        Script.registerValue("FLOWAPP", true);

        var TABLET_BUTTON_NAME = "FLOW";
        var HTML_URL = Script.resolvePath("./flowAppCpp.html");

        var SHOW_AVATAR = true;
        var SHOW_DEBUG_SHAPES = false;
        var SHOW_SOLID_SHAPES = false;
        var POLYLINE_TEXTURE = Script.resolvePath("./polylineTexture.png");

        var USE_COLLISIONS = false;
        var IS_ACTIVE = false;

        var MSG_DOCUMENT_LOADED = 0;
        var MSG_JOINT_INPUT_DATA = 1;
        var MSG_COLLISION_DATA = 2;
        var MSG_COLLISION_INPUT_DATA = 3;
        var MSG_DISPLAY_DATA = 4;
        var MSG_CREATE = 5;

        var avatarScale = MyAvatar.getSensorToWorldScale();

        var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system");
        var tabletButton = tablet.addButton({
            text: TABLET_BUTTON_NAME,
            icon: Script.resolvePath("./flow-i.svg"),
            activeIcon: Script.resolvePath("./flow-a.svg")
        });

        var FlowDebug = function() {
            var self = this;
            this.debugLines = {};
            this.debugSpheres = {};
            this.showDebugShapes = false;
            this.showSolidShapes = "lines"; // wireframe

            this.setDebugLine = function(lineName, norms, startPosition, endPosition, shapeColor, width, forceRendering) {
                var doRender = self.showDebugShapes || forceRendering;
                if (!doRender) {
                    return;
                }
                var start = startPosition ? startPosition : {x: 0, y: 0, z: 0};
                var end = endPosition ? endPosition : {x: 0, y: 1, z: 0};
                var color = shapeColor ? shapeColor : { red: 0, green: 255, blue: 255 };
                if (self.debugLines[lineName] !== undefined) {
                    Entities.editEntity(self.debugLines[lineName], {
                        color: color,
                        linePoints: [start, end],
                        strokeWidths: [width, width],
                    });
                } else {
                    self.debugLines[lineName] = Entities.addEntity({
                        type: "PolyLine",
                        textures: POLYLINE_TEXTURE,
                        color: color,
                        normals: norms,
                        linePoints: [start, end],
                        visible: true,
                        strokeWidths: [width, width],
                        collisionless: true
                    }, "local");
                }
            };

            this.setDebugSphere = function(sphereName, pos, diameter, shapeColor, forceRendering) {
                var doRender = self.showDebugShapes || forceRendering;
                if (!doRender) {
                    return;
                }
                var DEFAULT_SPHERE_DIAMETER = 0.02;
                var scale = diameter ? diameter : DEFAULT_SPHERE_DIAMETER;
                var color = shapeColor ? shapeColor : { red: 255, green: 0, blue: 255 };
                if (self.debugSpheres[sphereName] !== undefined) {
                    Entities.editEntity(self.debugSpheres[sphereName], {
                        color: color,
                        position: pos,
                        dimensions: {x: scale, y: scale, z: scale},
                        primitiveMode: self.showSolidShapes,
                    });
                } else {
                    self.debugSpheres[sphereName] = Entities.addEntity({
                        type: "Sphere",
                        color: color,
                        position: pos,
                        dimensions: {x: scale, y: scale, z: scale},
                        primitiveMode: self.showSolidShapes,
                        visible: true,
                        collisionless: true
                    }, "local");
                }
            };

            this.deleteSphere = function(name) {
                Entities.deleteEntity(self.debugSpheres[name]);
                self.debugSpheres[name] = undefined;
            };

            this.deleteLine = function(name) {
                Entities.deleteEntity(self.debugLines[name]);
                self.debugLines[name] = undefined;
            };

            this.cleanup = function() {
                for (var lineName in self.debugLines) {
                    if (lineName !== undefined) {
                        self.deleteLine(lineName);
                    }
                }
                for (var sphereName in self.debugSpheres) {
                    if (sphereName !== undefined) {
                        self.deleteSphere(sphereName);
                    }
                }
                self.debugLines = {};
                self.debugSpheres = {};
            };

            this.setVisible = function(isVisible) {
                self.showDebugShapes = isVisible;
                for (var lineName in self.debugLines) {
                    if (lineName !== undefined) {
                        Entities.editEntity(self.debugLines[lineName], {
                            visible: isVisible
                        });
                    }
                }
                for (var sphereName in self.debugSpheres) {
                    if (sphereName !== undefined) {
                        Entities.editEntity(self.debugSpheres[sphereName], {
                            visible: isVisible
                        });
                    }
                }
            };

            this.setSolid = function(isSolid) {
                if(isSolid) {
                    self.showSolidShapes = "solid"
                } else {
                    self.showSolidShapes = "lines"
                }
                for (var lineName in self.debugLines) {
                    if (lineName !== undefined) {
                        Entities.editEntity(self.debugLines[lineName], {
                            primitiveMode: self.showSolidShapes
                        });
                    }
                }
                for (var sphereName in self.debugSpheres) {
                    if (sphereName !== undefined) {
                        Entities.editEntity(self.debugSpheres[sphereName], {
                            primitiveMode: self.showSolidShapes
                        });
                    }
                }
            };

        };

        var flowData, initActive, initColliding, initDebugging;
        updateFlowData(true);
        var collisionDebug = new FlowDebug();
        var jointDebug = new FlowDebug();

        collisionDebug.setVisible(SHOW_DEBUG_SHAPES);
        collisionDebug.setSolid(SHOW_SOLID_SHAPES);

        MyAvatar.setEnableMeshVisible(SHOW_AVATAR);
        jointDebug.setVisible(SHOW_DEBUG_SHAPES);
        jointDebug.setSolid(SHOW_SOLID_SHAPES);

        var shown = false;

        function manageClick() {
            if (shown) {
                MyAvatar.useFlow(initActive, initColliding);
                initDebugging = SHOW_DEBUG_SHAPES;
                if (SHOW_DEBUG_SHAPES) {
                    toggleDebugShapes();
                }
                tablet.gotoHomeScreen();
            } else {
                updateFlowData();
                tablet.gotoWebScreen(HTML_URL);
            }
        }
        
        tabletButton.clicked.connect(manageClick);
      
        function onScreenChanged(type, url) {     
            console.log("Screen changed");
            if (type === "Web" && url === HTML_URL) {
                tabletButton.editProperties({isActive: true});
                if (!shown) {
                    // hook up to event bridge
                    tablet.webEventReceived.connect(onWebEventReceived);
                }
                shown = true;
            } else {
                tabletButton.editProperties({isActive: false});
                if (shown) {
                    // disconnect from event bridge
                    tablet.webEventReceived.disconnect(onWebEventReceived);
                }
                shown = false;
            }
        }
        
        var toggleAvatarVisible = function() {
            SHOW_AVATAR = !SHOW_AVATAR;
            MyAvatar.setEnableMeshVisible(SHOW_AVATAR);
        };
        
        var toggleDebugShapes = function() {
            SHOW_DEBUG_SHAPES = !SHOW_DEBUG_SHAPES;
            if (USE_COLLISIONS) {
                collisionDebug.setVisible(SHOW_DEBUG_SHAPES);
            }
            jointDebug.setVisible(SHOW_DEBUG_SHAPES);
        };
        
        var toggleSolidShapes = function() {
            SHOW_SOLID_SHAPES = !SHOW_SOLID_SHAPES;
            collisionDebug.setSolid(SHOW_SOLID_SHAPES);
            jointDebug.setSolid(SHOW_SOLID_SHAPES);
        };
        
        var toggleCollisions = function() {
            USE_COLLISIONS = !USE_COLLISIONS;
            if (USE_COLLISIONS && SHOW_DEBUG_SHAPES) {
                collisionDebug.setVisible(true);
            } else {
                collisionDebug.setVisible(false);
            }
            MyAvatar.useFlow(IS_ACTIVE, USE_COLLISIONS);
        };
        
        var getDisplayData = function() {
            return {"avatar": SHOW_AVATAR, 
                "collisions": USE_COLLISIONS, 
                "debug": SHOW_DEBUG_SHAPES, 
                "solid": SHOW_SOLID_SHAPES};
        };
        
        var jointNames = MyAvatar.getJointNames();
        
        function roundFloat(number, decimals) {
            var DECIMALS_MULTIPLIER = 10;
            var multiplier = Math.pow(DECIMALS_MULTIPLIER, decimals);
            var rounded = Math.round(number * multiplier);
            return rounded / multiplier;
        }
        
        function roundVector(vector, decimals) {
            return {x: roundFloat(vector.x, decimals), y: roundFloat(vector.y, decimals), z: roundFloat(vector.z, decimals)};
        }

        function roundDataValues(decimals) {
            var collisions = flowData.collisions;
            var physics = flowData.physics;
            Object.keys(collisions).forEach(function(key) {
                var data = collisions[key];
                data.radius = roundFloat(data.radius, decimals);
                data.offset = roundVector(data.offset, decimals);                
            });
            Object.keys(physics).forEach(function(key) {
                var data = physics[key];
                data.damping = roundFloat(data.damping, decimals);
                data.delta = roundFloat(data.delta, decimals);
                data.gravity = roundFloat(data.gravity, decimals);
                data.inertia = roundFloat(data.inertia, decimals);
                data.radius = roundFloat(data.radius, decimals);
                data.stiffness = roundFloat(data.stiffness, decimals);               
            });
        }

        function updateFlowData(catchInitValues) {
            flowData = MyAvatar.getFlowData();
            if (typeof(flowData) !== "object" || typeof(flowData.collisions) !== "object") {
                return;
            }
            var ROUND_DECIMALS = 4;
            var collisionJoints = Object.keys(flowData.collisions);
            var inverseScale = 1.0 / avatarScale;
            for (var i = 0; i < collisionJoints.length; i++) {
                var collision = flowData.collisions[collisionJoints[i]];
                collision.radius *= inverseScale;
                collision.offset = Vec3.multiply(collision.offset, inverseScale);
            }
            roundDataValues(ROUND_DECIMALS);
            IS_ACTIVE = flowData.active;
            USE_COLLISIONS = flowData.colliding;
            if (catchInitValues) {
                initActive = flowData.active;
                initColliding = flowData.colliding;
            }
        }
        
        function onWebEventReceived(msg) {
            var message = JSON.parse(msg);
            switch (message.type) {
                case MSG_JOINT_INPUT_DATA: {
                    flowData.physics[message.group][message.name] = message.value;
                    MyAvatar.useFlow(IS_ACTIVE, USE_COLLISIONS, flowData.physics, flowData.collisions);
                    break;
                }
                case MSG_COLLISION_INPUT_DATA: {
                    var value = message.name === "offset" ? {x: 0.0, y: message.value, z: 0.0} : message.value;
                    flowData.collisions[message.group][message.name] = value;
                    MyAvatar.useFlow(IS_ACTIVE, USE_COLLISIONS, flowData.physics, flowData.collisions);
                    break;
                }
                case MSG_DISPLAY_DATA: {
                    switch (message.name) {
                        case "collisions":
                            toggleCollisions();
                            break;
                        case "debug":
                            toggleDebugShapes();
                            break;
                        case "solid":
                            toggleSolidShapes();
                            break;
                        case "avatar":
                            toggleAvatarVisible();
                            break;
                    }
                    break;
                }
                case MSG_DOCUMENT_LOADED: {
                    MyAvatar.useFlow(true, true);
                    updateFlowData();
                    if (initDebugging && !SHOW_DEBUG_SHAPES) {
                        toggleDebugShapes();
                    }
                    createHTMLMenu();
                    break;
                }
                case MSG_COLLISION_DATA: {
                    switch (message.name) {
                        case "add":
                            var collisionData = {"type": "sphere", "radius": 0.05, "offset": {"x": 0.0, "y": 0.0, "z": 0.0}};
                            flowData.collisions[message.value] = collisionData;
                            MyAvatar.useFlow(IS_ACTIVE, USE_COLLISIONS, flowData.physics, flowData.collisions);
                            updateFlowData();
                            tablet.emitScriptEvent(JSON.stringify({
                                "type": MSG_COLLISION_DATA, 
                                "name": message.value,  
                                "data": collisionData
                            }));
                            break;
                        case "remove":
                            var jointName = message.value;
                            collisionDebug.deleteSphere(jointName + "_col");
                            if (flowData.collisions[jointName] !== undefined) {
                                delete flowData.collisions[jointName];
                                MyAvatar.useFlow(IS_ACTIVE, USE_COLLISIONS, flowData.physics, flowData.collisions);
                                updateFlowData();
                            }
                            break;
                    }
                    break;
                }
            }
        }

        tablet.screenChanged.connect(onScreenChanged);

        function createHTMLMenu() {
            jointNames = MyAvatar.getJointNames();
            tablet.emitScriptEvent(JSON.stringify(
                {
                    "type": MSG_CREATE,
                    "data": {
                        "display": getDisplayData(),
                        "group": flowData.physics,
                        "collisions": flowData.collisions,
                        "joints": jointNames
                    }
                }
            ));
        }

        function shutdownTabletApp() {
            MyAvatar.useFlow(initActive, initColliding);
            tablet.removeButton(tabletButton);
            if (shown) {
                tablet.webEventReceived.disconnect(onWebEventReceived);
                tablet.gotoHomeScreen();
            }
            tablet.screenChanged.disconnect(onScreenChanged);
        }
            
        MyAvatar.skeletonChanged.connect(function() {
            var MS_AFTER_AVATAR_UPDATE = 200;
            collisionDebug.cleanup();
            jointDebug.cleanup();
            Script.setTimeout(function() {
                jointNames = MyAvatar.getJointNames();
                avatarScale = MyAvatar.getSensorToWorldScale();
                updateFlowData(true);
                if (shown) {
                    manageClick();
                }
            }, MS_AFTER_AVATAR_UPDATE);
        });

        MyAvatar.scaleChanged.connect(function() {
            avatarScale = MyAvatar.getSensorToWorldScale();
        });
        
        Script.update.connect(function() {
            if (IS_ACTIVE) {
                var groupData = flowData.physics;
                var collisionData = flowData.collisions;
                var threads = flowData.threads;
                var groups = Object.keys(groupData);
                var flowPositions = Array(jointNames.length);
                var flowCollisionColors = Array(jointNames.length);
                var collidingJoints = MyAvatar.getCollidingFlowJoints();
                for (var i = 0; i < groups.length; i++) {
                    var group = groups[i];
                    var data = groupData[group];
                    for (var j = 0; j < data.jointIndices.length; j++) {
                        var index = data.jointIndices[j];
                        var name = jointNames[index];
                        var position = MyAvatar.getJointPosition(index);
                        flowPositions[index] = position;
                        var colliding = collidingJoints.indexOf(index) > -1;
                        var color = { red: 255, green: 255, blue: 0 };
                        if (colliding) {
                            color.green = 0;
                        }
                        flowCollisionColors[index] = color;
                        var radius = 2.0 * avatarScale * data.radius;
                        jointDebug.setDebugSphere(name + "_flow", position, radius, color);
                    }
                }
                var names = Object.keys(collisionData);
                for (i = 0; i < names.length; i++) {
                    name = names[i];
                    index = collisionData[name].jointIndex;
                    
                    var offset = Vec3.multiply(collisionData[name].offset, avatarScale);
                    radius = avatarScale * collisionData[name].radius;
                    position = MyAvatar.jointToWorldPoint(offset, index);
                    collisionDebug.setDebugSphere(name + "_col", position, 2 * radius, {red: 200, green: 10, blue: 50});
                }
                var threadKeys = Object.keys(threads);
                for (i = 0; i < threadKeys.length; i++) {
                    var thread = threads[threadKeys[i]];
                    for (j = 1; j < thread.length; j++) {
                        var index1 = thread[j-1];
                        var index2 = thread[j];
                        if (flowPositions[index1] !== undefined && flowPositions[index2] !== undefined) {
                            var lineName = jointNames[index1] + "_line";
                            color = flowCollisionColors[index1];
                            var DEFAULT_LINE_WIDTH = 0.004;
                            var lineWidth = DEFAULT_LINE_WIDTH * avatarScale;
                            // We are creating two PolyLines with different normals, to make them more visible from the sides.
                            var LINE_NORMALS_1 = [{ x: 0, y: 0, z: 1 }, { x: 0, y: 0, z: 1 }];
                            var LINE_NORMALS_2 = [{ x: 1, y: 0, z: 0 }, { x: 1, y: 0, z: 0 }];
                            jointDebug.setDebugLine(lineName + "_1", LINE_NORMALS_1, flowPositions[index1], flowPositions[index2], color, lineWidth);
                            jointDebug.setDebugLine(lineName + "_2", LINE_NORMALS_2, flowPositions[index1], flowPositions[index2], color, lineWidth);
                        }
                    }
                }
            }
        });

        Script.scriptEnding.connect(function () {
            collisionDebug.cleanup();
            jointDebug.cleanup();
            shutdownTabletApp();
        });
    }, MS_AFTER_LOADING);

}());