"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.PlanarSubdivision = void 0;
/*---------------------------------------------------------------------------------------------
* Copyright (c) Bentley Systems, Incorporated. All rights reserved.
* See LICENSE.md in the project root for license terms and full copyright notice.
*--------------------------------------------------------------------------------------------*/
const core_bentley_1 = require("@itwin/core-bentley");
const Geometry_1 = require("../../Geometry");
const Transform_1 = require("../../geometry3d/Transform");
const Graph_1 = require("../../topology/Graph");
const HalfEdgeGraphSearch_1 = require("../../topology/HalfEdgeGraphSearch");
const Merging_1 = require("../../topology/Merging");
const Arc3d_1 = require("../Arc3d");
const CurveLocationDetail_1 = require("../CurveLocationDetail");
const LineSegment3d_1 = require("../LineSegment3d");
const LineString3d_1 = require("../LineString3d");
const Loop_1 = require("../Loop");
const ParityRegion_1 = require("../ParityRegion");
const RegionOps_1 = require("../RegionOps");
const RegionOpsClassificationSweeps_1 = require("../RegionOpsClassificationSweeps");
/** @packageDocumentation
 * @module Curve
 */
function computeSortAngle(curve, fraction, reverse) {
    const ray = curve.fractionToPointAndDerivative(fraction);
    const s = reverse ? -1.0 : 1.0;
    return Math.atan2(s * ray.direction.y, s * ray.direction.x);
}
function getFractionOnCurve(pair, curve) {
    if (pair.detailA.curve === curve)
        return pair.detailA.fraction;
    if (pair.detailB.curve === curve)
        return pair.detailB.fraction;
    return undefined;
}
function getDetailOnCurve(pair, curve) {
    if (pair.detailA.curve === curve)
        return pair.detailA;
    if (pair.detailB.curve === curve)
        return pair.detailB;
    return undefined;
}
class MapCurvePrimitiveToCurveLocationDetailPairArray {
    primitiveToPair = new Map();
    // index assigned to this primitive (for debugging)
    primitiveToIndex = new Map();
    _numIndexedPrimitives = 0;
    assignPrimitiveIndex(primitive) {
        if (primitive !== undefined) {
            const index = this.primitiveToIndex.get(primitive);
            if (index === undefined) {
                this.primitiveToIndex.set(primitive, this._numIndexedPrimitives++);
            }
        }
    }
    insertPrimitiveToPair(primitive, pair) {
        if (primitive) {
            const priorPairs = this.primitiveToPair.get(primitive);
            this.assignPrimitiveIndex(primitive);
            if (!priorPairs) {
                this.primitiveToPair.set(primitive, [pair]);
            }
            else {
                priorPairs.push(pair);
            }
        }
    }
    insertPair(pair) {
        const primitiveA = pair.detailA.curve;
        if (primitiveA)
            this.insertPrimitiveToPair(primitiveA, pair);
        const primitiveB = pair.detailB.curve;
        if (primitiveB)
            this.insertPrimitiveToPair(primitiveB, pair);
    }
    /**
     * Split closed missing primitives in half and add new intersection pairs.
     * * When bridge edges aren't included in the primitives array, a closed primitive with no intersections will not be
     * added to the graph because it isn't in the `primitiveToPair` map. By splitting such a missing primitive in two, we
     * introduce two intersections for each half, which allows the primitive to be represented in the map.
     */
    splitAndAppendMissingClosedPrimitives(primitives, tolerance = Geometry_1.Geometry.smallMetricDistance) {
        for (const p of primitives) {
            let closedCurveSplitCandidate = false;
            if (p instanceof Arc3d_1.Arc3d)
                closedCurveSplitCandidate = p.sweep.isFullCircle;
            else if (!(p instanceof LineSegment3d_1.LineSegment3d) && !(p instanceof LineString3d_1.LineString3d)) // TODO: probably should do this for all types. Lots of spline-type primitives can be closed.
                closedCurveSplitCandidate = p.startPoint().isAlmostEqualXY(p.endPoint(), tolerance);
            if (closedCurveSplitCandidate && !this.primitiveToPair.has(p)) {
                const p0 = p.clonePartialCurve(0.0, 0.5);
                const p1 = p.clonePartialCurve(0.5, 1.0);
                if (p0 && p1) {
                    this.insertPair(CurveLocationDetail_1.CurveLocationDetailPair.createCapture(CurveLocationDetail_1.CurveLocationDetail.createCurveEvaluatedFraction(p0, 0.0), CurveLocationDetail_1.CurveLocationDetail.createCurveEvaluatedFraction(p1, 1.0)));
                    this.insertPair(CurveLocationDetail_1.CurveLocationDetailPair.createCapture(CurveLocationDetail_1.CurveLocationDetail.createCurveEvaluatedFraction(p0, 1.0), CurveLocationDetail_1.CurveLocationDetail.createCurveEvaluatedFraction(p1, 0.0)));
                }
            }
        }
    }
}
/**
 * @internal
 */
class PlanarSubdivision {
    /**
     * Create a graph from curves and precomputed intersections.
     * * Z-coordinates are ignored.
     * @param primitives input curves
     * @param allPairs array of curve-curve xy-intersections
     * @param mergeTolerance optional distance tolerance for clustering vertices. Default value is [[Geometry.smallMetricDistance]].
     */
    static assembleHalfEdgeGraph(primitives, allPairs, mergeTolerance = Geometry_1.Geometry.smallMetricDistance) {
        // map from key CurvePrimitive to CurveLocationDetailPair
        const detailByPrimitive = new MapCurvePrimitiveToCurveLocationDetailPairArray();
        for (const pair of allPairs)
            detailByPrimitive.insertPair(pair);
        if (primitives.length > detailByPrimitive.primitiveToPair.size) {
            // otherwise, these single-primitive loops are missing from the graph
            detailByPrimitive.splitAndAppendMissingClosedPrimitives(primitives, mergeTolerance);
        }
        const graph = new Graph_1.HalfEdgeGraph();
        for (const entry of detailByPrimitive.primitiveToPair.entries()) {
            const p = entry[0];
            // convert each interval intersection into two isolated intersections
            const details = entry[1].reduce((accumulator, detailPair) => {
                if (!detailPair.detailA.hasFraction1)
                    return [...accumulator, detailPair];
                const detail = getDetailOnCurve(detailPair, p);
                const detail0 = CurveLocationDetail_1.CurveLocationDetail.createCurveFractionPoint(p, detail.fraction, detail.point);
                const detail1 = CurveLocationDetail_1.CurveLocationDetail.createCurveFractionPoint(p, detail.fraction1, detail.point1);
                return [
                    ...accumulator,
                    CurveLocationDetail_1.CurveLocationDetailPair.createCapture(detail0, detail0),
                    CurveLocationDetail_1.CurveLocationDetailPair.createCapture(detail1, detail1),
                ];
            }, []);
            // lexical sort on p intersection fraction
            details.sort((pairA, pairB) => {
                const fractionA = getFractionOnCurve(pairA, p);
                const fractionB = getFractionOnCurve(pairB, p);
                return fractionA - fractionB;
            });
            let last = { point: p.startPoint(), fraction: 0.0 };
            for (const detailPair of details) {
                const detail = getDetailOnCurve(detailPair, p);
                const detailFraction = Geometry_1.Geometry.restrictToInterval(detail.fraction, 0, 1); // truncate fraction, but don't snap point; clustering happens later
                last = this.addHalfEdge(graph, p, last.point, last.fraction, detail.point, detailFraction, mergeTolerance);
            }
            this.addHalfEdge(graph, p, last.point, last.fraction, p.endPoint(), 1.0, mergeTolerance);
        }
        // every edge got its sortAngle defined by addHalfEdge
        Merging_1.HalfEdgeGraphMerge.clusterAndMergeXYTheta(graph, (he) => he.sortAngle ?? 0, mergeTolerance);
        return graph;
    }
    /**
     * Filter for trivial curve fragment: fast computation if line segment; otherwise clip, flatten, and measure.
     * * Different metrics are employed for expedience.
    */
    static isCurveTrivialXY(p, point0, fraction0, point1, fraction1, mergeTolerance) {
        if (Geometry_1.Geometry.isSmallRelative(fraction0 - fraction1))
            return true;
        if (!(p instanceof LineSegment3d_1.LineSegment3d)) {
            const p0 = p.clonePartialCurve(fraction0, fraction1);
            if (p0?.tryTransformInPlace(Transform_1.Transform.createRowValues(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0))) // flatten
                return p0.curveLength() <= mergeTolerance; // Euclidean
        }
        return point0.isAlmostEqualXY(point1, mergeTolerance); // Manhattan
    }
    /**
     * Create a pair of mated half edges referencing a non-trivial interval of a primitive.
     * @param graph containing graph
     * @param p the curve
     * @param point0 start point
     * @param fraction0 starting fraction
     * @param point1 end point
     * @param fraction1 end fraction
     * @param mergeTolerance optional maximum xy-length of a trivial edge
     * @returns end point and fraction
     */
    static addHalfEdge(graph, p, point0, fraction0, point1, fraction1, mergeTolerance = Geometry_1.Geometry.smallMetricDistance) {
        if (!this.isCurveTrivialXY(p, point0, fraction0, point1, fraction1, mergeTolerance)) {
            const halfEdge = graph.createEdgeXYAndZ(point0, 0, point1, 0);
            if (p.parent && p.parent instanceof RegionOpsClassificationSweeps_1.RegionGroupMember && p.parent.parentGroup.groupOpType === RegionOpsClassificationSweeps_1.RegionGroupOpType.NonBounding)
                halfEdge.setMaskAroundEdge(Graph_1.HalfEdgeMask.BRIDGE_EDGE);
            const detail01 = CurveLocationDetail_1.CurveLocationDetail.createCurveEvaluatedFractionFraction(p, fraction0, fraction1);
            const mate = halfEdge.edgeMate;
            halfEdge.edgeTag = detail01;
            halfEdge.sortData = 1.0;
            mate.edgeTag = detail01;
            mate.sortData = -1.0;
            halfEdge.sortAngle = computeSortAngle(p, fraction0, false);
            mate.sortAngle = computeSortAngle(p, fraction1, true);
        }
        return { point: point1, fraction: fraction1 }; // where the next curve fragment starts
    }
    /**
     * Based on computed (and toleranced) area, push the loop (pointer) onto the appropriate array of positive, negative,
     * or sliver loops.
     * @param zeroAreaTolerance absolute area tolerance for sliver face detection
     * @param isSliverFace whether the loop is known a priori (e.g., via topology) to have zero area
     * @returns the area (forced to zero if within tolerance)
     */
    static collectSignedLoop(loop, outLoops, zeroAreaTolerance = 1.0e-10, isSliverFace) {
        let area = isSliverFace ? 0.0 : RegionOps_1.RegionOps.computeXYArea(loop);
        if (area === undefined)
            area = 0;
        if (Math.abs(area) < zeroAreaTolerance)
            area = 0.0;
        loop.computedAreaInPlanarSubdivision = area;
        if (area > 0)
            outLoops.positiveAreaLoops.push(loop);
        else if (area < 0)
            outLoops.negativeAreaLoops.push(loop);
        else
            outLoops.slivers.push(loop);
        return area;
    }
    /** Extract geometric info from a topological edge. */
    static extractGeometryFromEdge(edge) {
        if (edge.sortData !== undefined && edge.edgeTag && edge.edgeTag instanceof CurveLocationDetail_1.CurveLocationDetail) {
            const detail = edge.edgeTag;
            if (detail.curve && detail.fraction1 !== undefined) {
                const reversed = edge.sortData < 0;
                return { detail, reversed };
            }
        }
        return undefined;
    }
    /** Create the geometry for a topological edge. */
    static createCurveInEdge(edge, z) {
        let result;
        const info = this.extractGeometryFromEdge(edge);
        if (info && info.detail.curve && info.detail.fraction1) {
            const f0 = info.reversed ? info.detail.fraction1 : info.detail.fraction;
            const f1 = info.reversed ? info.detail.fraction : info.detail.fraction1;
            result = info.detail.curve.clonePartialCurve(f0, f1);
            if (result && z !== undefined)
                result.tryTransformInPlace(Transform_1.Transform.createRowValues(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, z));
        }
        return result;
    }
    /**
     * Create a [[Loop]] for the given face or super face.
     * @param face a node in the face loop, or an array of HalfEdges that comprise a loop (e.g., a super face).
     * @param options bundle of options.
     * @returns the Loop, or `undefined` if it is not closed within xy-tolerance.
     */
    static createLoopInFace(face, options) {
        const consolidate = options?.announceEdge ? false : options?.compress ?? false; // can't compress if announcing
        const loop = Loop_1.Loop.create();
        const addEdgeCurve = (edge) => {
            const curve = this.createCurveInEdge(edge, options?.z);
            if (curve) {
                options?.announceEdge?.(edge, curve, loop);
                loop.tryAddChild(curve);
            }
        };
        if (Array.isArray(face))
            face.forEach(addEdgeCurve);
        else
            face.announceEdgesInFace(addEdgeCurve);
        if (consolidate) {
            const consolidateOptions = new RegionOps_1.ConsolidateAdjacentCurvePrimitivesOptions();
            consolidateOptions.consolidateLoopSeam = true;
            RegionOps_1.RegionOps.consolidateAdjacentPrimitives(loop, consolidateOptions);
        }
        if (loop.isPhysicallyClosedCurve(options?.closureTol, true))
            return loop;
        (0, core_bentley_1.assert)(() => false, "face is not physically closed");
        return undefined;
    }
    /**
     * Create a [[Loop]] or [[ParityRegion]] for the given face.
     * * A ParityRegion is created for a split-washer type face by removing bridge edges.
     * @param face a node in the face loop.
     * @param options bundle of options.
     * @returns the Loop or ParityRegion, or `undefined` if one could not be computed.
     */
    static createLoopOrParityRegionInFace(face, options) {
        let region;
        const visitMask = options?.visitMask ?? Graph_1.HalfEdgeMask.VISITED;
        const bridgeMask = options?.bridgeMask ?? Graph_1.HalfEdgeMask.BRIDGE_EDGE;
        let startBridgeEdge;
        // is it a split-washer face?
        if (face.isSplitWasherFace(bridgeMask) && (startBridgeEdge = face.findMaskAroundFace(bridgeMask, true))) {
            const loops = [];
            const loopEdges = [];
            const bridgeStack = [startBridgeEdge];
            const announceEdge = (he) => { he.setMask(visitMask); loopEdges.push(he); };
            const announceBridge = (he) => { if (!he.isMaskSet(visitMask))
                bridgeStack.push(he); };
            face.clearMaskAroundFace(visitMask);
            let bridge;
            while (undefined !== (bridge = bridgeStack.pop())) {
                bridge.setMask(visitMask);
                const loopSeed = bridge.findMaskAroundFace(bridgeMask, false); // advance to next loop
                if (loopSeed) {
                    if (loopSeed.isMaskSet(visitMask))
                        continue;
                    loopEdges.length = 0;
                    if (loopSeed.announceEdgesInSuperFace(bridgeMask, announceEdge, announceBridge)) {
                        const loop = this.createLoopInFace(loopEdges, options);
                        if (loop) {
                            loops.push(loop);
                            continue;
                        }
                    }
                }
            }
            region = RegionOps_1.RegionOps.sortOuterAndHoleLoopsXY(loops);
            region = RegionOps_1.RegionOps.simplifyRegion(region);
        }
        else {
            region = this.createLoopInFace(face, options);
        }
        return (region && (region instanceof Loop_1.Loop || region instanceof ParityRegion_1.ParityRegion)) ? region : undefined;
    }
    /** Return true if there are only two edges in the face loop, and their start curvatures are the same. */
    static isNullFace(he) {
        const faceHasTwoEdges = (he.faceSuccessor.faceSuccessor === he);
        let faceIsBanana = false;
        if (faceHasTwoEdges) {
            const c0 = Merging_1.HalfEdgeGraphMerge.curvatureSortKey(he);
            const c1 = Merging_1.HalfEdgeGraphMerge.curvatureSortKey(he.faceSuccessor.edgeMate);
            if (!Geometry_1.Geometry.isSameCoordinate(c0, c1)) // default tol!
                faceIsBanana = true; // heuristic: we could also check end curvatures, and/or higher derivatives...
        }
        return faceHasTwoEdges && !faceIsBanana;
    }
    /** Look across edge mates (possibly several) for a non-null mate face. */
    static getNonNullEdgeMate(_graph, e) {
        if (this.isNullFace(e))
            return undefined;
        let e1 = e.edgeMate;
        while (this.isNullFace(e1)) {
            e1 = e1.faceSuccessor.edgeMate;
            if (e1 === e)
                return undefined;
        }
        return e1;
    }
    static collectSignedLoopSetsInHalfEdgeGraph(graph, zeroAreaTolerance = 1.0e-10) {
        const q = HalfEdgeGraphSearch_1.HalfEdgeGraphSearch.collectConnectedComponentsWithExteriorParityMasks(graph, undefined);
        const result = [];
        const edgeMap = new Map();
        for (const faceSeeds of q) {
            const componentAreas = { positiveAreaLoops: [], negativeAreaLoops: [], slivers: [] };
            const edges = [];
            for (const faceSeed of faceSeeds) {
                const isNullFace = this.isNullFace(faceSeed);
                const announceEdge = isNullFace ? undefined : (he, curveC, loopC) => {
                    const mate = this.getNonNullEdgeMate(graph, he);
                    if (mate !== undefined) {
                        const e = edgeMap.get(mate);
                        if (e === undefined) {
                            // Record this as loopA,edgeA of a shared edge to be completed later from the other side of the edge
                            const e1 = new Loop_1.LoopCurveLoopCurve(loopC, curveC, undefined, undefined);
                            edgeMap.set(he, e1);
                        }
                        else if (e instanceof Loop_1.LoopCurveLoopCurve) {
                            e.setB(loopC, curveC);
                            edges.push(e);
                            edgeMap.delete(mate);
                        }
                    }
                };
                const loop = this.createLoopInFace(faceSeed, { announceEdge });
                if (loop)
                    this.collectSignedLoop(loop, componentAreas, zeroAreaTolerance, isNullFace);
            }
            componentAreas.edges = edges;
            result.push(componentAreas);
            edgeMap.clear();
        }
        return result;
    }
}
exports.PlanarSubdivision = PlanarSubdivision;
//# sourceMappingURL=PlanarSubdivision.js.map