import * as Core from "@intelligentgraphics/ig.gfx.core";
import * as Planning from "@intelligentgraphics/ig.gfx.planning";
import * as ThreeJS from "@intelligentgraphics/ig.gfx.threejs";
import * as THREE from "@intelligentgraphics/three";
import { closestWallWithRotation } from "./Util";
// LinearAlgebra.AsPlane -> convertPlaneSegmentToPlane in Core 3.2.0
const convertPlaneSegmentToPlane = "AsPlane" in Core.LinearAlgebra
    ? Core.LinearAlgebra["AsPlane"]
    : Core.convertPlaneSegmentToPlane;
export class HolePuncher {
    constructor(roomContainer, api) {
        this.roomContainer = roomContainer;
        this.api = api;
    }
    /**
     * called in move start: remove holes of currently selected articles, then rerender
     */
    fillHolesOfSelected() {
        const tSelecteds = this.api.GetAllSelectedComponents();
        if (tSelecteds) {
            const tWallIds = Planning.Environment.getInstance().getWalls();
            const tAllWalls = [];
            tWallIds.forEach(wi => {
                tAllWalls.push(this.api.GetComponent(wi));
            });
            // remove any holes caused by currently selecteds
            tAllWalls.forEach(w => {
                if (!w.userData || !w.userData.punchedHoles) {
                    return;
                }
                const remainingHoles = w.userData.punchedHoles.filter(hole => !tSelecteds.some(ss => ss === hole.causedById));
                w.userData.punchedHoles = remainingHoles;
                //Logger.info("recreate ", w.name, w.userData.punchedHoles);
                this.recreateWall(w.name, w.userData.punchedHoles);
            });
            this.api.RedrawScene();
        }
    }
    /**
     * Adds a new hole for every explicitId (or selected) into nearest wall (if applicable) and renders only the new changed walls
     */
    punchHolesIntoNearestWall(componentIds) {
        const changedWalls = [];
        componentIds.forEach(sid => {
            if (this.api
                .GetCategories(sid)
                .indexOf(Core.Category.CuttingThroughWall) < 0) {
                return;
            }
            //2 find nearest wall
            const tWallId = closestWallWithRotation(sid);
            if (!tWallId) {
                return;
            }
            const tWallDistance = Planning.WallUtilities.closestWallDistance(sid);
            //TODO IMPORTANT  THIS MIGHT BE WRONG -> ASK SH about correct distance calculation for rotated objects
            //or wall connection of a given accessory object
            if (tWallDistance < 0.3) {
                //Logger.warn("close wallid: ", tWallId, " dist: ", tWallDistance);
                //3 calculate hole middle
                const tGAB = ThreeJS.AbstractBoundingBox.CreateFromGfxId(sid);
                // const tGAB = /*this.api.*/ GetAbstractBoundsH.GetAbstractBoundsX(
                // sid
                // );
                const tSelectedBase = tGAB.getDirectionBasePoint();
                let tSelectedCenter = tSelectedBase;
                const tDirs = tGAB.getDirections();
                tDirs.forEach(d => {
                    tSelectedCenter = Core.Vector.Add3(tSelectedCenter, Core.Vector.ScalarMultiply3(0.5, d));
                });
                const frontFace = Planning.WallUtilities.faces(tWallId).front;
                const tWallPlane = convertPlaneSegmentToPlane(frontFace);
                const backFace = Planning.WallUtilities.faces(tWallId).back;
                const tWallPlaneB = convertPlaneSegmentToPlane(backFace);
                const tIntersect = Core.LinearAlgebra.GetIntersectionPoint({
                    origin: tSelectedCenter,
                    direction: tWallPlane.normal,
                }, tWallPlane) ||
                    Core.LinearAlgebra.GetIntersectionPoint({
                        origin: tSelectedCenter,
                        direction: Core.Vector.Reverse3(tWallPlane.normal),
                    }, tWallPlane);
                if (!tIntersect) {
                    return;
                }
                try {
                    const tUpperWindowBorder = tIntersect.y +
                        (Core.Vector.Magnitude3(tDirs[1]) / 2 - 0.001);
                    const tWall = this.api.GetComponent(tWallId);
                    if (tUpperWindowBorder > tWall.userData.wallInfo.Height) {
                        return;
                    }
                    const tOR = tGAB.getDirectionBasePoint();
                    const tUp = Core.Vector.Add3(tOR, tDirs[1]);
                    const tRight = Core.Vector.Add3(tOR, tDirs[0]);
                    const tIntersectOR = Core.LinearAlgebra.GetIntersectionPoint({ origin: tOR, direction: tWallPlane.normal }, tWallPlane) ||
                        Core.LinearAlgebra.GetIntersectionPoint({
                            origin: tOR,
                            direction: Core.Vector.Reverse3(tWallPlane.normal),
                        }, tWallPlane);
                    const tIntersectUP = Core.LinearAlgebra.GetIntersectionPoint({ origin: tUp, direction: tWallPlane.normal }, tWallPlane) ||
                        Core.LinearAlgebra.GetIntersectionPoint({
                            origin: tUp,
                            direction: Core.Vector.Reverse3(tWallPlane.normal),
                        }, tWallPlane);
                    const tIntersectRight = Core.LinearAlgebra.GetIntersectionPoint({
                        origin: tRight,
                        direction: tWallPlane.normal,
                    }, tWallPlane) ||
                        Core.LinearAlgebra.GetIntersectionPoint({
                            origin: tRight,
                            direction: Core.Vector.Reverse3(tWallPlane.normal),
                        }, tWallPlane);
                    //find intersect point with backface to make sure hole fits into both planes: front and back, especially for polygonal rooms
                    const tIntersectORB = Core.LinearAlgebra.GetIntersectionPoint({ origin: tOR, direction: tWallPlaneB.normal }, tWallPlaneB) ||
                        Core.LinearAlgebra.GetIntersectionPoint({
                            origin: tOR,
                            direction: Core.Vector.Reverse3(tWallPlaneB.normal),
                        }, tWallPlaneB);
                    const tIntersectUPB = Core.LinearAlgebra.GetIntersectionPoint({ origin: tUp, direction: tWallPlaneB.normal }, tWallPlaneB) ||
                        Core.LinearAlgebra.GetIntersectionPoint({
                            origin: tUp,
                            direction: Core.Vector.Reverse3(tWallPlaneB.normal),
                        }, tWallPlaneB);
                    const tIntersectRightB = Core.LinearAlgebra.GetIntersectionPoint({
                        origin: tRight,
                        direction: tWallPlaneB.normal,
                    }, tWallPlaneB) ||
                        Core.LinearAlgebra.GetIntersectionPoint({
                            origin: tRight,
                            direction: Core.Vector.Reverse3(tWallPlaneB.normal),
                        }, tWallPlaneB);
                    tIntersectOR.y += 0.001;
                    tIntersectUP.y += 0.001;
                    tIntersectRight.y += 0.001;
                    tIntersectORB.y += 0.001;
                    tIntersectUPB.y += 0.001;
                    tIntersectRightB.y += 0.001;
                    if (Core.LinearAlgebra.IsInsidePlaneSegment(frontFace, tIntersectOR) &&
                        Core.LinearAlgebra.IsInsidePlaneSegment(frontFace, tIntersectUP) &&
                        Core.LinearAlgebra.IsInsidePlaneSegment(frontFace, tIntersectRight) &&
                        Core.LinearAlgebra.IsInsidePlaneSegment(backFace, tIntersectORB) &&
                        Core.LinearAlgebra.IsInsidePlaneSegment(backFace, tIntersectUPB) &&
                        Core.LinearAlgebra.IsInsidePlaneSegment(backFace, tIntersectRightB)) {
                        // everything inside smaller wall mesh --> OK
                    }
                    else {
                        //Logger.info("new test found conflict (despite added height) -> canceled");
                        return;
                    }
                    //Logger.info("inside test finished ok");
                }
                catch (e) {
                    //Logger.info("wall: ", tWall);
                }
                const tHole = {
                    causedById: sid,
                    position: new THREE.Vector3(tIntersect.x, tIntersect.y, tIntersect.z),
                    halfSize: new THREE.Vector3(Core.Vector.Magnitude3(tDirs[0]) / 2 - 0.001, 
                    //height slightly less to fix doors , width slightly less to fix 2 neighbors
                    Core.Vector.Magnitude3(tDirs[1]) / 2 - 0.001, 1),
                };
                const tWall = this.api.GetComponent(tWallId);
                if (!tWall.userData) {
                    tWall.userData = {};
                }
                if (!tWall.userData.punchedHoles) {
                    tWall.userData.punchedHoles = [];
                }
                //remove hole if it already existed -> necessary for depth change via property editor
                let tWasAlready = false;
                tWall.userData.punchedHoles.forEach((ph) => {
                    if (ph.causedById === tHole.causedById) {
                        ph.halfSize = tHole.halfSize;
                        ph.position = tHole.position;
                        tWasAlready = true;
                    }
                });
                if (!tWasAlready) {
                    tWall.userData.punchedHoles.push(tHole);
                }
                if (!changedWalls.some(w => w.name === tWallId)) {
                    //not already inside, so push into
                    changedWalls.push(tWall);
                }
            }
        });
        // data updated, now render only changed walls
        changedWalls.forEach(w => {
            this.recreateWall(w.name, w.userData.punchedHoles);
        });
        this.api.RedrawScene();
    }
    /**
     *
     * @param wallComponentId
     * @param wallItems
     *
     * Often called like (w.name, w.userData.punchedHoles) for redraw
     */
    recreateWall(wallComponentId, wallItems) {
        const w = this.api.GetComponent(wallComponentId);
        const ms = [];
        // find meshes interior and exterior
        w.traverse(node => {
            if (node instanceof THREE.Mesh) {
                // insert your code here, for example:
                //node.material = new THREE.MeshNormalMaterial()
                if (node["SpecialMeshName"] === "Exterior" ||
                    node["SpecialMeshName"] === "Interior") {
                    ms.push(node);
                }
            }
        });
        ms.forEach(m => {
            const tStart = m["SpecialMeshOriginalStart"];
            const tEnd = m["SpecialMeshOriginalEnd"];
            const tTransform = m["SpecialMeshOriginalTransform"];
            const tInvTransform = m["SpecialMeshOriginalInvTransform"];
            const tHeight = m["SpecialMeshOriginalHeight"];
            // z is thickness :-/
            const MX = this.makeWallX(tStart, tEnd, tTransform, tInvTransform, undefined, tHeight, wallItems);
            m.geometry = MX.geometry; //  new THREE.ShapeGeometry(shape);
            const tSpecialImportMatrix = m["SpecialImportMatrix"];
            const tSpecialMeshToOriginRotateMatrix = m["SpecialMeshToOriginRotateMatrix"];
            //Logger.info(tSpecialImportMatrix, tSpecialMeshToOriginRotateMatrix);
            if (tSpecialImportMatrix != undefined) {
                m.geometry.applyMatrix(tSpecialImportMatrix); //tMatrix
                m.geometry.verticesNeedUpdate = true;
                m.updateMatrix();
                m.geometry.applyMatrix(tSpecialMeshToOriginRotateMatrix); //MeshToOriginRotate
                m.geometry.verticesNeedUpdate = true;
                m.updateMatrix();
            }
        });
    }
    toVec3(pos, height) {
        height = height || 0;
        return new THREE.Vector3(pos.x, height, pos.y);
    }
    /** Gets the distance of two points.
     * @param x1 X part of first point.
     * @param y1 Y part of first point.
     * @param x2 X part of second point.
     * @param y2 Y part of second point.
     * @returns The distance.
     */
    calculateDistanceBetweenPoints(x1, y1, x2, y2) {
        return Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2));
    }
    // start, end have x and y attributes (i.e. corners)
    makeWallX(start, end, transform, invTransform, material, wallHeight, wallItems) {
        //Logger.info("makeWall ", start, end, invTransform, material);
        const v1 = this.toVec3(start);
        const v2 = this.toVec3(end);
        const v3 = v2.clone();
        v3.y = wallHeight;
        const v4 = v1.clone();
        v4.y = wallHeight;
        const points = [v1.clone(), v2.clone(), v3.clone(), v4.clone()];
        points.forEach(p => {
            p.applyMatrix4(transform);
        });
        const shape = new THREE.Shape([
            new THREE.Vector2(points[0].x, points[0].y),
            new THREE.Vector2(points[1].x, points[1].y),
            new THREE.Vector2(points[2].x, points[2].y),
            new THREE.Vector2(points[3].x, points[3].y),
        ]);
        // add holes for each wall item
        wallItems.forEach(item => {
            const pos = item.position.clone();
            //Logger.info("addWall punching hole ", item.position, item.halfSize);
            pos.applyMatrix4(transform);
            const halfSize = item.halfSize;
            const min = halfSize.clone().multiplyScalar(-1);
            const max = halfSize.clone();
            min.add(pos);
            max.add(pos);
            const holePoints = [
                new THREE.Vector2(min.x, min.y),
                new THREE.Vector2(max.x, min.y),
                new THREE.Vector2(max.x, max.y),
                new THREE.Vector2(min.x, max.y),
            ];
            //Logger.info("holepoints:" + JSON.stringify(holePoints));
            shape.holes.push(new THREE.Path(holePoints)); //reverse doesnt fix problem
        });
        const geometry = new THREE.ShapeGeometry(shape);
        geometry.vertices.forEach(v => {
            v.applyMatrix4(invTransform);
        });
        const translation = new THREE.Vector3();
        const rotationq = new THREE.Quaternion();
        const scale = new THREE.Vector3();
        invTransform.decompose(translation, rotationq, scale);
        const rotation = new THREE.Euler(0, 0, 0, "XYZ");
        rotation.setFromQuaternion(rotationq, "XYZ");
        // make UVs
        const totalDistance = this.calculateDistanceBetweenPoints(v1.x, v1.z, v2.x, v2.z);
        const height = wallHeight;
        geometry.faceVertexUvs[0] = [];
        const vertexToUv = (vertex) => {
            const x = this.calculateDistanceBetweenPoints(v1.x, v1.z, vertex.x, vertex.z) /*  /  totalDistance */; //NM experiment
            const y = vertex.y /*  / height */; //NM experiment
            return new THREE.Vector2(x, y);
        };
        geometry.faces.forEach(face => {
            const vertA = geometry.vertices[face.a];
            const vertB = geometry.vertices[face.b];
            const vertC = geometry.vertices[face.c];
            geometry.faceVertexUvs[0].push([
                vertexToUv(vertA),
                vertexToUv(vertB),
                vertexToUv(vertC),
            ]);
            //Logger.info("UV generated: write UV: ", JSON.stringify(geometry.faceVertexUvs[0]));
        });
        geometry.faceVertexUvs[1] = geometry.faceVertexUvs[0];
        geometry.computeFaceNormals();
        geometry.computeVertexNormals();
        const mesh = new THREE.Mesh(geometry, material);
        mesh["SpecialMeshOriginalShape"] = shape;
        mesh["SpecialMeshOriginalStart"] = start;
        mesh["SpecialMeshOriginalEnd"] = end;
        mesh["SpecialMeshOriginalTransform"] = transform;
        mesh["SpecialMeshOriginalInvTransform"] = invTransform;
        mesh["SpecialMeshOriginalHeight"] = wallHeight;
        //material not exported , instead use already existing material, or create with null and extract geometry
        return mesh;
    }
}
