import { __read, __spread, __values } from "tslib";
import _ from "lodash";
import * as Core from "@intelligentgraphics/ig.gfx.core";
import { LinearAlgebra } from "@intelligentgraphics/ig.gfx.core";
import { WallUtilities } from "../WallUtilities";
import Environment from "../Environment";
import { StandardPlane, NonEnvironmentWallPlane, WallPlane, } from "./MovementPlanes";
/**
 * Class for converting mouse movement to movement in the 3D space.
 * It uses planes to which the movement of components is restricted.
 */
var MouseTo3DMovement = /** @class */ (function () {
    /**
     * Class for converting mouse movement to movement in the 3D space.
     * It uses planes to which the movement of components is restricted.
     * @param converter
     * @param planes: Consists of main planes and non-main planes.
     * 				  These have to be constructed so that for any given ray from the camera,
     * 				  there is always at least one intersection with a plane.
     */
    function MouseTo3DMovement(converter, planes) {
        this.converter = converter;
        this.planes = planes;
    }
    /**
     * Transfer movement in the camera plane (ViewCoordinates) to movement
     * in the movement planes. For this, look at the ray the camera emits
     * (through the current coordinates) and intersect it with all the planes.
     * If there is an intersection in a main plane, take the closest
     * one to the camera. Otherwise do the same for the other planes,
     * and project the result to the first main Plane.
     * @param coordinates the current coordinates on screen
     * @returns {Core.WorldCoordinates} the calculated position
     */
    MouseTo3DMovement.prototype.transferCoordinates = function (coordinates) {
        var mainPlanes = this.planes.filter(function (p) { return p.isMainPlane; });
        var secondaryPlanes = this.planes.filter(function (p) { return !p.isMainPlane; });
        var intersections = this.sortedIntersections(coordinates, mainPlanes);
        if (intersections.length > 0) {
            return intersections[0];
        }
        var secondaryIntersections = this.sortedIntersections(coordinates, secondaryPlanes);
        // movement is only allowed in the main plane, so project the best match
        // to the first main plane
        return LinearAlgebra.ProjectToPlane(secondaryIntersections[0], mainPlanes[0].plane);
    };
    /**
     * Get the intersections ordered by distance to the camera.
     * @param coordinates
     * @param planes
     * @returns {Array<WorldCoordinates>}
     */
    MouseTo3DMovement.prototype.sortedIntersections = function (coordinates, planes) {
        var validIntersections = this.validIntersections(coordinates, planes);
        return this.sortIntersections(validIntersections);
    };
    /**
     * Get all valid intersections that the camera ray has with the planes.
     * @param coordinates
     * @param movementPlanes
     * @returns {Array<WorldCoordinates>}
     */
    MouseTo3DMovement.prototype.validIntersections = function (coordinates, movementPlanes) {
        var e_1, _a;
        var cameraRay = this.converter.ViewToWorldRay(coordinates);
        var result = [];
        try {
            for (var movementPlanes_1 = __values(movementPlanes), movementPlanes_1_1 = movementPlanes_1.next(); !movementPlanes_1_1.done; movementPlanes_1_1 = movementPlanes_1.next()) {
                var movementPlane = movementPlanes_1_1.value;
                var intersectionPoint = LinearAlgebra.GetIntersectionPoint(cameraRay, movementPlane.plane);
                // const cameraDirection = this.converter.CameraDirection();
                var cameraDirection = cameraRay.direction;
                var angleCameraToPlaneNormal = Core.Vector.AngleBetweenVectors3(Core.Vector.Reverse3(cameraDirection), movementPlane.plane.normal);
                var angleThreshold = (2 * Math.PI) / 180;
                var angleMinus90 = Math.PI / 2 - angleCameraToPlaneNormal;
                if (Math.abs(angleMinus90) > angleThreshold) {
                    if (movementPlane.isValid(intersectionPoint)) {
                        result.push(intersectionPoint);
                    }
                }
            }
        }
        catch (e_1_1) { e_1 = { error: e_1_1 }; }
        finally {
            try {
                if (movementPlanes_1_1 && !movementPlanes_1_1.done && (_a = movementPlanes_1.return)) _a.call(movementPlanes_1);
            }
            finally { if (e_1) throw e_1.error; }
        }
        return result;
    };
    /**
     * Sort the intersections by distance to the camera.
     * @param intersections
     * @returns {Array<vec3>}
     */
    MouseTo3DMovement.prototype.sortIntersections = function (intersections) {
        var cameraPosition = this.converter.CameraPosition();
        var result = _.sortBy(intersections, function (point) {
            return Core.Vector.Distance3(point, cameraPosition);
        });
        return result;
    };
    return MouseTo3DMovement;
}());
export { MouseTo3DMovement };
/**
 * Class that decides on the movement restriction for the passed objects.
 * Constructs instances of the IMovementPlanes interface.
 */
var MovementPlaneDecider = /** @class */ (function () {
    function MovementPlaneDecider() {
    }
    /**
     * Decide if the group of ids can be moved on a wall
     * @param ids
     * @param orthographic
     * @returns {boolean}
     */
    MovementPlaneDecider.isEligibleForWallMovement = function (ids, orthographic) {
        // In orthographic view (from front) it is difficult to differentiate between
        // wall planes and the camera plane we want to move in the wall plane,
        // but not in the camera plane.
        // Thus we only activate the wall plane if we are close enough.
        // Otherwise the objects would jump from the middle of the room to the wall
        if (orthographic &&
            ids.some(function (id) { return WallUtilities.closestWallDistance(id) >= 0.01; })) {
            return false;
        }
        var result = ids.every(function (id) {
            return Core.Category.hasCategory(id, Core.Category.Floating);
        });
        return result;
    };
    /**
     * Get the movement planes for the current situation
     * @param converter
     * @param mouseAttachment
     * @param ids
     * @param keepFloating
     * @returns {IMovementPlane[]}
     */
    MovementPlaneDecider.planes = function (converter, mouseAttachment, ids, keepFloating) {
        var _this = this;
        // move the lowest element back to the ground
        var minimalHeight = ids
            .map(function (id) { return Core.State.API.GetPosition(id).y; })
            .reduce(function (min, height) { return Math.min(min, height); }, Infinity);
        var isOrthographic = converter.IsOrthographic();
        var areAllFloating = ids.every(function (id) {
            return _.includes(Core.State.API.GetCategories(id), Core.Category.Floating);
        });
        var keepHeight = areAllFloating && keepFloating;
        var heightAdaptedPoint = Core.Vector.Sub3(mouseAttachment, new Core.Float3(0, keepHeight ? 0 : minimalHeight, 0));
        var xzPlane = new StandardPlane({ normal: new Core.Float3(0, 1, 0), point: heightAdaptedPoint }, true);
        // camera plane is only for backup, e.g. in orthographic view
        var cameraPlane = new StandardPlane({ point: mouseAttachment, normal: converter.CameraDirection() }, false);
        if (MovementPlaneDecider.isEligibleForWallMovement(ids, isOrthographic)) {
            var environmentWalls_1 = Environment.getInstance().getWalls();
            var environmentWallPlanes = environmentWalls_1.map(function (wallId) {
                return new WallPlane(wallId, _this.wallOffset(mouseAttachment, ids, wallId));
            });
            var rootIds = Core.State.API.GetRoots();
            var nonEnvironmentWalls = rootIds.filter(function (root) {
                var categories = Core.State.API.GetCategories(root);
                var isWall = _.includes(categories, Core.Category.Wall);
                if (!isWall) {
                    return false;
                }
                var isEnvironmentWall = _.includes(environmentWalls_1, root);
                return !isEnvironmentWall;
            });
            var otherWallPlanes = nonEnvironmentWalls.map(function (wallId) {
                return new NonEnvironmentWallPlane(wallId, _this.wallOffset(mouseAttachment, ids, wallId));
            });
            // Putting xz before wall planes, thus if something is hit in the camera plane,
            // we will project back to the ground and not the wall
            return __spread([
                xzPlane
            ], environmentWallPlanes, otherWallPlanes, [
                cameraPlane,
            ]);
        }
        return [xzPlane, cameraPlane];
    };
    /**
     * Calculate the offset for the wall plane.
     * This is necessary so that the closest point of the bounding box is always directly at the wall.
     * @param mouseAttachment
     * @param ids
     * @param wallId
     * @returns {WorldCoordinates}
     */
    MovementPlaneDecider.wallOffset = function (mouseAttachment, ids, wallId) {
        var areAllCuttingThroughWall = ids.every(function (component) {
            return Core.Category.hasCategory(component, Core.Category.CuttingThroughWall);
        });
        if (areAllCuttingThroughWall) {
            var positionOfId0 = Core.State.API.GetPosition(ids[0]);
            var result_1 = Core.Vector.Sub3(mouseAttachment, positionOfId0);
            return result_1;
        }
        var points = _.flatMap(ids, function (id) {
            return Core.State.API.GetAbstractBounds(id).getPoints();
        });
        var closestPoint = _.sortBy(points, function (pt) {
            return WallUtilities.distanceToWall(pt, wallId);
        })[0];
        var result = Core.Vector.Sub3(mouseAttachment, closestPoint);
        return result;
    };
    return MovementPlaneDecider;
}());
export { MovementPlaneDecider };
