"use strict";
/*---------------------------------------------------------------------------------------------
* Copyright (c) Bentley Systems, Incorporated. All rights reserved.
* See LICENSE.md in the project root for license terms and full copyright notice.
*--------------------------------------------------------------------------------------------*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.HalfEdgeGraphSearch = exports.HalfEdgeMaskTester = void 0;
/** @packageDocumentation
 * @module Topology
 */
const Range_1 = require("../geometry3d/Range");
const Graph_1 = require("./Graph");
const SignedDataSummary_1 = require("./SignedDataSummary");
const XYParitySearchContext_1 = require("./XYParitySearchContext");
/**
 * Class to test match of half edge mask.
 * @internal
 */
class HalfEdgeMaskTester {
    _targetMask;
    _targetValue;
    /**
     * Constructor
     * @param mask mask to test in `testEdge` function
     * @param targetValue value to match for true return
     */
    constructor(mask, targetValue = true) {
        this._targetMask = mask;
        this._targetValue = targetValue;
    }
    /** Return true if the value of the targetMask matches the targetValue. */
    testEdge(edge) {
        return edge.isMaskSet(this._targetMask) === this._targetValue;
    }
}
exports.HalfEdgeMaskTester = HalfEdgeMaskTester;
/**
 * Class for different types of searches for HalfEdgeGraph.
 *  @internal
 */
class HalfEdgeGraphSearch {
    /**
     * Static method for face area computation -- useful as function parameter in `collectFaceAreaSummary`.
     * * This simply calls `node.signedFaceArea()`
     * @param node instance for signedFaceArea call.
     */
    static signedFaceArea(node) {
        return node.signedFaceArea();
    }
    /**
     * Return a summary of face data (e.g., area) as computed by the callback on the faces of the graph.
     * * Callers with curved edge graphs must supply their own area function.
     * @param source graph or array of nodes to examine.
     * @param collectAllNodes flag to pass to the `SignedDataSummary` constructor to control collection of nodes.
     * @param areaFunction function to obtain area (or other numeric value). Default computes polygonal face area.
     */
    static collectFaceAreaSummary(source, collectAllNodes = false, areaFunction = (node) => HalfEdgeGraphSearch.signedFaceArea(node)) {
        const result = new SignedDataSummary_1.SignedDataSummary(collectAllNodes);
        let allFaces;
        if (source instanceof Graph_1.HalfEdgeGraph)
            allFaces = source.collectFaceLoops();
        else
            allFaces = source;
        for (const node of allFaces) {
            const area = areaFunction(node);
            result.announceItem(node, area);
        }
        return result;
    }
    /**
     * Search the graph for the face with the most negative area.
     * * If the graph has exactly one connected component, this is its outer face.
     * @param oneCandidateNodePerFace graph or an array containing one node from each face to be considered.
     * @returns node on the negative area face with largest absolute area, or `undefined` if no negative area face.
     */
    static findMinimumAreaFace(oneCandidateNodePerFace, faceAreaFunction) {
        const summary = HalfEdgeGraphSearch.collectFaceAreaSummary(oneCandidateNodePerFace, false, faceAreaFunction);
        return summary.largestNegativeItem;
    }
    /**
     * Test if the graph is triangulated.
     * * Return `false` if:
     *   * number of positive area faces with more than 3 edges is larger than `numPositiveExceptionsAllowed`.
     *   * graph has more than 1 negative area face when `allowMultipleNegativeAreaFaces` is `false`.
     * * 2-edge faces are ignored.
     */
    static isTriangulatedCCW(source, allowMultipleNegativeAreaFaces = true, numPositiveExceptionsAllowed = 0) {
        let allFaces;
        if (source instanceof Graph_1.HalfEdgeGraph)
            allFaces = source.collectFaceLoops();
        else
            allFaces = source;
        let numNegative = 0;
        let numPositiveExceptions = 0;
        for (const node of allFaces) {
            const numEdges = node.countEdgesAroundFace();
            if (numEdges >= 3) {
                const area = node.signedFaceArea();
                if (area > 0) {
                    if (numEdges > 3) {
                        numPositiveExceptions++;
                        if (numPositiveExceptions > numPositiveExceptionsAllowed)
                            return false;
                    }
                }
                else {
                    numNegative++;
                    if (numNegative > 1) {
                        if (!allowMultipleNegativeAreaFaces)
                            return false;
                    }
                }
            }
        }
        return true;
    }
    /**
     * Process a face during graph traversal.
     * @param faceSeed a node in the face.
     * @param mask mask to set on each node of the face.
     * @param allNodeStack array appended with each node of the face.
     * @param onePerFaceStack array appended with `faceSeed`.
     */
    static pushAndMaskAllNodesInFace(faceSeed, mask, allNodeStack, onePerFaceStack) {
        onePerFaceStack.push(faceSeed);
        faceSeed.collectAroundFace((node) => {
            node.setMask(mask);
            allNodeStack.push(node);
        });
    }
    /**
     * Traverse (via Depth First Search) to all accessible faces from the given seed.
     * @param faceSeed first node to start the traverse.
     * @param visitMask mask applied to all faces as visited.
     * @param parityEdgeTester function to test if an edge is adjacent to two faces of opposite parity, e.g., a boundary
     * edge that separates an "interior" face and an "exterior" face. If `parityEdgeTester` is not supplied and `parityMask`
     * is supplied, the default parity rule is to alternate parity state in a "bullseye" pattern starting at the seed face,
     * with each successive concentric ring of faces at constant topological distance from the seed face receiving the
     * opposite parity state of the previous ring.
     * @param parityMask mask to apply to the first face and faces that share the same parity as the first face, as
     * determined by the parity rule. If this is `NULL_MASK`, there is no record of parity. If (non-null) parity mask
     * is given, on return it is entirely set or entirely clear around each face.
     * @returns an array that contains one representative node in each face of the connected component.
     */
    static parityFloodFromSeed(faceSeed, visitMask, parityEdgeTester, parityMask) {
        const faces = [];
        if (faceSeed.isMaskSet(visitMask))
            return faces; // empty array
        const allMasks = parityMask | visitMask;
        const stack = [];
        // the seed face is arbitrarily assigned the parity mask
        HalfEdgeGraphSearch.pushAndMaskAllNodesInFace(faceSeed, allMasks, stack, faces);
        while (stack.length > 0) {
            const p = stack.pop();
            const mate = p.edgeMate;
            if (!mate)
                continue;
            if (!mate.isMaskSet(visitMask)) {
                let newState = p.isMaskSet(parityMask);
                if (!parityEdgeTester || parityEdgeTester.testEdge(p))
                    newState = !newState;
                HalfEdgeGraphSearch.pushAndMaskAllNodesInFace(mate, newState ? allMasks : visitMask, stack, faces);
            }
        }
        return faces;
    }
    /**
     * * Correct the parity mask in the faces of a component.
     * * It is assumed that the parity mask is applied _consistently_ throughout the supplied faces, but maybe
     * not _correctly_.
     * * A consistently applied parity mask is "correct" if it is set on the negative area ("exterior") face of
     * a connected component.
     * * This method finds a face with negative area and toggles the mask throughout the input faces if this face
     * lacks the parity mask.
     * * In a properly merged planar subdivision there should be only one true negative area face per component.
     */
    static correctParityInSingleComponent(parityMask, faces) {
        const exteriorHalfEdge = HalfEdgeGraphSearch.findMinimumAreaFace(faces);
        if (!exteriorHalfEdge) {
            // graph has all degenerate faces; do nothing
        }
        else if (exteriorHalfEdge.isMaskSet(parityMask)) {
            // all should be well; nothing to do
        }
        else {
            for (const faceSeed of faces) {
                if (faceSeed.isMaskSet(parityMask)) {
                    faceSeed.clearMaskAroundFace(parityMask);
                }
                else {
                    faceSeed.setMaskAroundFace(parityMask);
                }
            }
        }
    }
    /** Apply `correctParityInSingleComponent` to each array in components (quick exit if `parityMask` is `NULL_MASK`). */
    static correctParityInComponentArrays(parityMask, components) {
        if (parityMask === Graph_1.HalfEdgeMask.NULL_MASK)
            return;
        for (const facesInComponent of components)
            HalfEdgeGraphSearch.correctParityInSingleComponent(parityMask, facesInComponent);
    }
    /**
     * Collect connected components of the graph (via Depth First Search).
     * @param graph graph to inspect.
     * @param parityEdgeTester (optional) function to test if an edge is adjacent to two faces of opposite parity,
     * e.g., a boundary edge that separates an "interior" face and an "exterior" face. If `parityEdgeTester` is not
     * supplied and `parityMask` is supplied, the default parity rule is to alternate parity state in a "bullseye"
     * pattern starting at the seed face, with each successive concentric ring of faces at constant topological
     * distance from the seed face receiving the opposite parity state of the previous ring.
     * @param parityMask (optional) mask to apply to the first face and faces that share the same parity as the
     * first face, as determined by the parity rule. If this is `NULL_MASK` (default), there is no record of parity.
     * If (non-null) parity mask is given, on return it is entirely set or entirely clear around each face.
     * @returns the components of the graph, each component represented by an array of nodes, one node per face
     * of the component. In other words, entry [i][j] is a HalfEdge in the j_th face loop of the i_th component.
     */
    static collectConnectedComponentsWithExteriorParityMasks(graph, parityEdgeTester, parityMask = Graph_1.HalfEdgeMask.NULL_MASK) {
        // Illustration of the algorithm can be found at geometry/internaldocs/Graph.md
        const components = [];
        const visitMask = Graph_1.HalfEdgeMask.VISITED;
        const allMasks = parityMask | visitMask;
        graph.clearMask(allMasks);
        for (const faceSeed of graph.allHalfEdges) {
            if (!faceSeed.isMaskSet(visitMask)) {
                const newFaces = HalfEdgeGraphSearch.parityFloodFromSeed(faceSeed, visitMask, parityEdgeTester, parityMask);
                // parityFloodFromSeed does not return an empty array because it is called on an unvisited faceSeed
                components.push(newFaces);
            }
        }
        HalfEdgeGraphSearch.correctParityInComponentArrays(parityMask, components);
        return components;
    }
    /**
     * Breadth-first search through the connected component defined by `seed` and `ignoreMask`.
     * @param seed a HalfEdge in the component from which to start the search.
     * @param visitMask a mask to apply to visited nodes. Assumed cleared throughout component before first call.
     * @param ignoreMask (optional) mask preset on faces that are to be treated as _outside_ the component.
     * Default value is `HalfEdgeMask.EXTERIOR` to ignore exterior faces.
     * Pass `HalfEdgeMask.NULL_MASK` to process all faces of the component.
     * Note that if `ignoreMask` is set on both sides of an edge, both adjacent faces will be ignored; thus it
     * is generally more useful if set on only one side of an edge.
     * @param maxFaceCount (optional) maximum number of faces to search in the component. If nonpositive or
     * `undefined`, `Infinity` is used.
     * @param result optional array to append with HalfEdges and return as the `faces` array. This array is not
     * emptied first, so callers can send it into a subsequent call to continue searching the component at `nextSeed`.
     * @returns `faces` consists of one edge per face of the component; `nextSeed` is the seed at which to start
     * the next call if `maxFaceCount` is exceeded (otherwise it is `undefined`).
     */
    static exploreComponent(seed, visitMask, ignoreMask = Graph_1.HalfEdgeMask.EXTERIOR, maxFaceCount = Infinity, result) {
        if (maxFaceCount <= 0)
            maxFaceCount = Infinity;
        const boundaryMask = visitMask | ignoreMask;
        let numFaces = 0;
        const candidates = []; // the queue
        const faces = result ? result : [];
        candidates.push(seed);
        while (candidates.length > 0 && numFaces < maxFaceCount) {
            // TODO: shift is O(n) thus inefficient for large queues. If needed, we can
            // replace the queue by a circular array or use 2 stacks; both are O(1).
            const node = candidates.shift();
            if (node && !node.isMaskSet(boundaryMask)) {
                faces.push(node);
                ++numFaces;
                node.collectAroundFace((he) => {
                    he.setMask(visitMask);
                    const neighbor = he.vertexSuccessor;
                    if (!neighbor.isMaskSet(boundaryMask))
                        candidates.push(neighbor); // enqueue neighboring face
                });
            }
        }
        if (candidates.length === 0)
            return { faces, nextSeed: undefined };
        // try to find a node at the boundary of the current component and the next
        let nextSeed = candidates[0];
        for (const candidate of candidates) {
            if (candidate.vertexSuccessor.isMaskSet(ignoreMask) || candidate.edgeMate.isMaskSet(ignoreMask)) {
                nextSeed = candidate;
                break;
            }
        }
        return { faces, nextSeed };
    }
    /**
     * Collect connected components of the graph (via Breadth First Search).
     * @param graph graph to inspect.
     * @param maxFaceCount (optional) maximum number of faces in each component. Should be positive; otherwise
     * `Infinity` is used.
     * @param ignoreMask (optional) mask preset on faces to ignore. Default value is `HalfEdgeMask.EXTERIOR` to ignore
     * exterior faces. Pass `HalfEdgeMask.NULL_MASK` to process all faces.
     * @returns the components of the graph, each component represented by an array of nodes, one node per face
     * of the component. In other words, entry [i][j] is a HalfEdge in the j_th face loop of the i_th component.
     */
    static collectConnectedComponents(graph, maxFaceCount = Infinity, ignoreMask = Graph_1.HalfEdgeMask.EXTERIOR) {
        const components = [];
        if (graph.countMask(ignoreMask) === 0)
            ignoreMask = Graph_1.HalfEdgeMask.NULL_MASK;
        const visitMask = Graph_1.HalfEdgeMask.VISITED;
        graph.clearMask(visitMask);
        const boundaryMask = visitMask | ignoreMask;
        // Starting with the input node, look ahead for a boundary face. Failing that, return the input node.
        // Starting all floods at the boundary reduces the chance of ending up with a ring-shaped component at the boundary.
        const findNextFloodSeed = (index) => {
            for (let i = index; i < graph.countNodes(); ++i) {
                if (!graph.allHalfEdges[i].isMaskSet(boundaryMask)
                    && graph.allHalfEdges[i].edgeMate.isMaskSet(boundaryMask)) {
                    index = i;
                    break;
                }
            }
            return index;
        };
        for (let i = 0; i < graph.countNodes(); ++i) {
            if (graph.allHalfEdges[i].isMaskSet(boundaryMask))
                continue;
            const i0 = findNextFloodSeed(i);
            let seed = graph.allHalfEdges[i0];
            do { // flood this component
                const result = HalfEdgeGraphSearch.exploreComponent(seed, visitMask, ignoreMask, maxFaceCount);
                seed = result.nextSeed;
                if (result.faces.length > 0)
                    components.push(result.faces); // each sub-component of length <= maxFaceCount is treated as a component
            } while (seed !== undefined);
            if (!graph.allHalfEdges[i].isMaskSet(visitMask))
                --i; // reprocess this node
        }
        return components;
    }
    /**
     * Test if test point (xTest,yTest) is inside/outside a face or on an edge.
     * * NOTE: a point outside the graph (in the exterior face) returns -1.
     * @param seedNode any node on the face loop.
     * @param xTest x coordinate of the test point.
     * @param yTest y coordinate of the test point.
     * @returns 0 if ON, 1 if IN, -1 if OUT.
     */
    static pointInOrOnFaceXY(seedNode, xTest, yTest) {
        const context = new XYParitySearchContext_1.XYParitySearchContext(xTest, yTest);
        // walk around looking for an accepted node to start the search (seedNode is usually ok)
        let nodeA = seedNode;
        let nodeB = seedNode.faceSuccessor;
        for (;; nodeA = nodeB) {
            if (context.tryStartEdge(nodeA.x, nodeA.y, nodeB.x, nodeB.y))
                break;
            if (nodeB === seedNode) {
                // the test point and the face are all on line "y = yTest"
                const range = Range_1.Range1d.createXX(nodeB.x, nodeB.faceSuccessor.x);
                return range.containsX(xTest) ? 0 : -1;
            }
            nodeB = nodeA.faceSuccessor;
        }
        // nodeB is the real start node for search, so stop when we revisit it. For each edge, accumulate parity and hits
        let nodeC = nodeB.faceSuccessor;
        for (;;) {
            if (!context.advance(nodeC.x, nodeC.y)) {
                return context.classifyCounts();
            }
            if (nodeC === nodeB)
                break;
            nodeC = nodeC.faceSuccessor;
        }
        return context.classifyCounts();
    }
    /**
     * Collect boundary edges starting from `seed`.
     * * If `seed` is not a boundary node or is already visited, the function exists early.
     * @param seed start node.
     * @param visitMask mask to set on processed nodes.
     * @param isBoundaryEdge function to test if an edge in a boundary edge.
     * @param announceEdgeInBoundary callback invoked on each edge in the boundary loop in order. The counter is zero
     * for the first edge, and incremented with each successive edge.
     */
    static collectExtendedBoundaryLoopFromSeed(seed, visitMask, isBoundaryEdge, announceEdgeInBoundary) {
        let counter = 0;
        while (!seed.getMask(visitMask) && isBoundaryEdge(seed)) {
            announceEdgeInBoundary(seed, counter++);
            seed.setMask(visitMask);
            const vertexBase = seed.faceSuccessor;
            let candidateAroundVertex = vertexBase;
            for (;;) {
                if (candidateAroundVertex.getMask(visitMask)) // end of boundary loop
                    return;
                if (isBoundaryEdge(candidateAroundVertex)) {
                    seed = candidateAroundVertex;
                    break;
                }
                candidateAroundVertex = candidateAroundVertex.vertexPredecessor;
                if (candidateAroundVertex === vertexBase)
                    break; // prevent infinite loop in case exteriorMask is not set on the edge mate of the boundary edge
            }
        }
    }
    /**
     * Collect boundary edges in the graph.
     * * A boundary edge is defined by `exteriorMask` being set on only its "exterior" edge mate.
     * * Each boundary edge is identified in the output by its edge mate that lacks `exteriorMask`.
     * * Each inner array is ordered in the output so that its boundary edges form a connected path. If `exteriorMask`
     * is preset consistently around each "exterior" face, these paths are loops.
     * @param graph the graph to query
     * @param exteriorMask mask preset on exactly one side of boundary edges
     * @returns array of boundary loops, each loop an array of the unmasked mates of boundary edges
     */
    static collectExtendedBoundaryLoopsInGraph(graph, exteriorMask) {
        // Illustration of the algorithm can be found at geometry/internaldocs/Graph.md
        const loops = [];
        const visitMask = graph.grabMask(true);
        const isBoundaryEdge = (edge) => {
            return edge.getMask(exteriorMask) === 0 && edge.edgeMate.getMask(exteriorMask) !== 0;
        };
        const announceEdgeInBoundary = (edge, counter) => {
            if (counter === 0)
                loops.push([]);
            loops[loops.length - 1].push(edge);
        };
        for (const seed of graph.allHalfEdges) {
            this.collectExtendedBoundaryLoopFromSeed(seed, visitMask, isBoundaryEdge, announceEdgeInBoundary);
        }
        graph.dropMask(visitMask);
        return loops;
    }
}
exports.HalfEdgeGraphSearch = HalfEdgeGraphSearch;
//# sourceMappingURL=HalfEdgeGraphSearch.js.map