/*---------------------------------------------------------------------------------------------
* Copyright (c) Bentley Systems, Incorporated. All rights reserved.
* See LICENSE.md in the project root for license terms and full copyright notice.
*--------------------------------------------------------------------------------------------*/
/** @packageDocumentation
 * @module CartesianGeometry
 */
import { BSplineCurve3d } from "../bspline/BSplineCurve";
import { Arc3d } from "../curve/Arc3d";
import { CurveCollection } from "../curve/CurveCollection";
import { CurveLocationDetail, CurveLocationDetailPair } from "../curve/CurveLocationDetail";
import { CurvePrimitive } from "../curve/CurvePrimitive";
import { LineSegment3d } from "../curve/LineSegment3d";
import { LineString3d } from "../curve/LineString3d";
import { Angle } from "../geometry3d/Angle";
import { Vector3d } from "../geometry3d/Point3dVector3d";
import { Point3dArray } from "../geometry3d/PointHelpers";
import { PolygonOps } from "../geometry3d/PolygonOps";
import { Range1d } from "../geometry3d/Range";
import { Range1dArray } from "../numerics/Range1dArray";
import { ClipPlane } from "./ClipPlane";
import { ClipUtilities } from "./ClipUtils";
import { ConvexClipPlaneSet } from "./ConvexClipPlaneSet";
/**
 * An AlternatingConvexClipTreeNode is a node in a tree structure in which
 *   <ul>
 *   <li>Each node contains a ConvexClipPlaneSet.
 *   <li>Each node contains an array of children which are also AlternatingConvexClipTreeNode.
 *   <li>The rule for an in/out decision is that a point is IN the subtree under a node if
 *   <ul>
 *   <li>It is IN the node's ConvexClipPlaneSet.
 *   <li>It is NOT IN any of the children.
 *   </ul>
 *   <li>Applying "NOT IN any of the children" locally to children at each level means that the ConvexClipPlaneSet
 *       at adjacent levels flip between being positive areas and holes.
 *   <li>Use an AlternatingConvexClipTreeNodeBuilder to construct the tree from a polygon.
 *   <li>It is possible for the root clip plane set to be empty.  An empty clip plane set returns "true"
 *         for all point tests, so the meaning is just that holes are to be subtracted from the rest
 *         of space.
 *   <li>Although the interpretation of in/out alternates with tree levels, the ConvexClipPlaneSets
 *         at each level are all "enclosing" planes in the usual way.
 *   </ul>
 */
export class AlternatingCCTreeNode {
    points = [];
    planes = ConvexClipPlaneSet.createEmpty();
    children = [];
    startIdx = -1; // Start index into the master array (not the local points array)
    numPoints = -1; // Number of points used in the master array
    constructor() { }
    /** Initialize this node with index data referencing the parent polygon. */
    static createWithIndices(index0, numPoints, result) {
        result = result ? result : new AlternatingCCTreeNode();
        result.startIdx = index0;
        result.numPoints = numPoints;
        result.children.length = 0;
        return result;
    }
    /**
     * <ul>
     * <li>Build the tree for a polygon.
     * <li>Caller creates the root node with empty constructor AlternatingConvexClipTreeNode.
     * </ul>
     */
    static createTreeForPolygon(points, result) {
        result = result ? result : new AlternatingCCTreeNode();
        result.empty();
        const builder = AlternatingCCTreeBuilder.createPointsRef(points);
        builder.buildHullTree(result); // <-- Currently ALWAYS returns true
        return result;
    }
    /** Build the outer convex hull with inlets as first level children. */
    static createHullAndInletsForPolygon(points, result) {
        result = result ? result : new AlternatingCCTreeNode();
        result.empty();
        const builder = AlternatingCCTreeBuilder.createPointsRef(points);
        builder.buildHullAndInletsForPolygon(result); // <-- Currently ALWAYS returns true
        return result;
    }
    extractLoopsGo(loops) {
        loops.push(Point3dArray.clonePoint3dArray(this.points));
        for (const c of this.children)
            c.extractLoopsGo(loops);
    }
    /**
     * Return an array with all the loops in the tree.
     * This loses the alternating structure of the tree, but the collection still matches well-formed polygons by
     * parity rules.
     */
    extractLoops() {
        const loops = [];
        this.extractLoopsGo(loops);
        return loops;
    }
    /** Resets this AlternatingConvexClipTreeNode to a newly-created state. */
    empty() {
        this.points.length = 0;
        this.planes.planes.length = 0;
        this.children.length = 0;
        this.startIdx = -1;
        this.numPoints = -1;
    }
    /** Creates a deep copy of this node (expensive - copies Geometry, and is recursive for children array). */
    clone(result) {
        result = result ? result : new AlternatingCCTreeNode();
        for (const point of this.points)
            result.points.push(point.clone());
        result.planes = ConvexClipPlaneSet.createEmpty();
        for (const plane of this.planes.planes)
            result.planes.planes.push(plane.clone());
        for (const node of this.children)
            result.children.push(node.clone());
        result.startIdx = this.startIdx;
        result.numPoints = this.numPoints;
        return result;
    }
    /** Add a new child that has an empty plane set and given indices. */
    addEmptyChild(index0, numPoints) {
        const newNode = AlternatingCCTreeNode.createWithIndices(index0, numPoints);
        this.children.push(newNode);
    }
    /** Add a plane to the ConvexClipPlaneSet. */
    addPlane(plane) {
        this.planes.addPlaneToConvexSet(plane);
    }
    /** Search with alternating in and out semantics. */
    isPointOnOrInside(point) {
        const inRoot = this.planes.isPointOnOrInside(point, 0.0);
        if (!inRoot)
            return false;
        for (const child of this.children) {
            if (child.isPointOnOrInside(point))
                return false;
        }
        return true;
    }
    /**
     * Add an AlternatingConvexClipTreeNode as a child of this one -- i.e. a hole.
     * * The child pointer is pushed directly to the tree -- not cloned.
     */
    captureConvexClipPlaneSetAsVoid(child) {
        this.children.push(child);
    }
    /** Append start-end positions for curve intervals classified as inside or outside. */
    appendCurvePrimitiveClipIntervals(curve, insideIntervals, outsideIntervals) {
        const clipper = new AlternatingCCTreeNodeCurveClipper();
        clipper.appendSingleClipPrimitive(this, curve, insideIntervals, outsideIntervals);
    }
    /** Append start-end positions for curve intervals classified as inside or outside. */
    appendCurveCollectionClipIntervals(curves, insideIntervals, outsideIntervals) {
        const clipper = new AlternatingCCTreeNodeCurveClipper();
        clipper.appendCurveCollectionClip(this, curves, insideIntervals, outsideIntervals);
    }
    /**
     * @param xyz input polygon. This is not changed.
     * @param insideFragments Array to receive "inside" fragments. Each fragment is a GrowableXYZArray grabbed from
     * the cache. This is NOT cleared.
     * @param outsideFragments Array to receive "outside" fragments. Each fragment is a GrowableXYZArray grabbed
     * from the cache. This is NOT cleared.
     * @param arrayCache cache for reusable GrowableXYZArray.
     */
    appendPolygonClip(xyz, insideFragments, outsideFragments, arrayCache) {
        // At first level ..
        // newInside is subject to re-clip by children.
        // outside is definitively outside
        const oldOutsideCount = outsideFragments.length;
        const newInside = this.planes.clipInsidePushOutside(xyz, outsideFragments, arrayCache);
        if (newInside === undefined) {
            ClipUtilities.restoreSingletonInPlaceOfMultipleShards(outsideFragments, oldOutsideCount, xyz, arrayCache);
        }
        else {
            let carryForwardA = [newInside];
            let carryForwardB = [];
            let tempAB;
            let shard;
            for (const c of this.children) {
                carryForwardB.length = 0;
                while (undefined !== (shard = carryForwardA.pop())) {
                    // Anything inside this child is truly outside  ...
                    c.appendPolygonClip(shard, outsideFragments, carryForwardB, arrayCache);
                    arrayCache.dropToCache(shard);
                }
                tempAB = carryForwardB;
                carryForwardB = carryForwardA; // and that is empty
                carryForwardA = tempAB;
            }
            while (undefined !== (shard = carryForwardA.pop())) {
                insideFragments.push(shard);
            }
        }
    }
    depth() {
        const myDepth = 1;
        let maxChildDepth = 0;
        for (const c of this.children) {
            maxChildDepth = Math.max(maxChildDepth, c.depth());
        }
        return myDepth + maxChildDepth;
    }
}
/**
 *  Context structure for building an AlternatingConvexClipTreeNode from a polygon.
 *  <ul>
 *  <li> The polygon is copied to the local m_points structure.
 *  <li> During construction, m_stack contains indices of a sequence of points with uniform concavity.
 *  </ul>
 */
export class AlternatingCCTreeBuilder {
    _points = [];
    _stack = [];
    constructor() { }
    static createPointsRef(points, result) {
        result = result ? result : new AlternatingCCTreeBuilder();
        result._points = points;
        if (PolygonOps.areaXY(points) < 0.0)
            result._points.reverse();
        if (result._points[result._points.length - 1].isAlmostEqualMetric(result._points[0]))
            result._points.pop();
        return result;
    }
    get period() {
        return this._points.length;
    }
    indexAfter(i) {
        return (i + 1) % this._points.length;
    }
    indexBefore(i) {
        return (i + this._points.length - 1) % this._points.length;
    }
    pushIndex(primaryPointIndex) {
        this._stack.push(primaryPointIndex);
    }
    static cross(pointA, pointB, pointC) {
        return pointA.crossProductToPointsXY(pointB, pointC);
    }
    /*
      public isInsideTurn(pointA: Point3d, pointB: Point3d, pointC: Point3d, sign: number) {
        return sign * AlternatingCCTreeBuilder.cross(pointA, pointB, pointC) > 0;
      }
    */
    cyclicStackPoint(cyclicIndex) {
        let stackIndex;
        const stack = this._stack;
        if (cyclicIndex > 0)
            stackIndex = cyclicIndex;
        else
            stackIndex = cyclicIndex + 10 * stack.length;
        stackIndex = stackIndex % stack.length;
        return this._points[stack[stackIndex]];
    }
    signFromStackTip(pointIndex, sign) {
        const pointA = this.cyclicStackPoint(-2);
        const pointB = this.cyclicStackPoint(-1);
        const pointC = this._points[pointIndex];
        return sign * AlternatingCCTreeBuilder.cross(pointA, pointB, pointC) >= 0.0 ? 1 : -1;
    }
    /*
     * Test of xyz is in the convex region bounded by stack points:
     * <ul>
     *   <li>polygon[i0]..polygon[i1]
     *   <li>polygon[j0]..polygon[j1]
     *   <li>polygon[i0]..polygon[i1]
     * </ul>
     * with "inside" controlled by sign multiplier.
    public isConvexContinuation(point: Point3d, i0: number, i1: number, j0: number, j1: number, sign: number): boolean {
      const points = this.points;
      const stack = this.stack;
      return this.isInsideTurn(points[stack[i0]], points[stack[i1]], point, sign)
          && this.isInsideTurn(points[stack[i0]], points[stack[j0]], point, sign)
          && this.isInsideTurn(points[stack[j1]], points[stack[i1]], point, sign);
    }
     */
    get indexOfMaxX() {
        let k = 0;
        const points = this._points;
        const nPoints = this._points.length;
        for (let i = 1; i < nPoints; i++) {
            if (points[i].x > points[k].x)
                k = i;
        }
        return k;
    }
    /** Pop from the stack until the sign condition is satisfied */
    extendHullChain(k, sign, pushAfterPops) {
        while (this._stack.length > 1 && this.signFromStackTip(k, sign) < 0.0)
            this._stack.pop();
        if (pushAfterPops)
            this.pushIndex(k);
    }
    collectHullChain(kStart, numK, sign) {
        this._stack.length = 0;
        if (numK > 2) {
            let k = kStart;
            for (let i = 0; i < numK; i++) {
                this.extendHullChain(k, sign, true);
                k = this.indexAfter(k);
            }
        }
    }
    collectHullPointsInArray(points, kStart, numK, _sign) {
        points.length = 0;
        if (numK > 2) {
            let k = kStart;
            for (let i = 0; i < numK; i++) {
                points.push(this._points[k]);
                k = this.indexAfter(k);
            }
        }
    }
    buildHullTreeGo(root, isPositiveArea, recurseToChildren = true) {
        this.collectHullChain(root.startIdx, root.numPoints, isPositiveArea ? 1.0 : -1.0);
        root.points.length = 0;
        const stack = this._stack;
        const points = this._points;
        const stackLen = stack.length;
        for (let i = 0; i < stackLen; i++) {
            const k0 = stack[i];
            root.points.push(points[k0]);
            if (i + 1 < stackLen) {
                let k1 = stack[i + 1];
                if (k1 === this.indexAfter(k0)) {
                    // two original points in sequence -- need a clip plane right here!!!
                    const plane = ClipPlane.createEdgeAndUpVector(points[k0], points[k1], Vector3d.create(0, 0, 1), Angle.createRadians(0));
                    if (plane !== undefined) {
                        if (isPositiveArea)
                            plane.negateInPlace();
                        root.addPlane(plane);
                    }
                }
                else {
                    if (k1 < k0)
                        k1 += this.period;
                    root.addEmptyChild(k0, k1 - k0 + 1);
                }
            }
        }
        if (recurseToChildren) {
            for (const child of root.children)
                this.buildHullTreeGo(child, !isPositiveArea);
        }
        else {
            for (const child of root.children)
                this.collectHullPointsInArray(child.points, child.startIdx, child.numPoints, isPositiveArea ? -1.0 : 1.0);
        }
        return true; // Are there failure modes? What happens with crossing data?..
    }
    /**
     * <ul>
     * <li> Input a ClipTreeRoot that has start and count data
     * <li> Build the hull for that data range
     * <li> Store the hull points in the root
     * <li> Add children with start and count data
     * <li> Recursively move to children
     * </ul>
     */
    buildHullAndInletsForPolygon(root) {
        AlternatingCCTreeNode.createWithIndices(this.indexOfMaxX, this.period + 1, root);
        return this.buildHullTreeGo(root, true, false);
    }
    /**
     * <ul>
     * <li> Input a ClipTreeRoot that has start and count data
     * <li> Build the hull for that data range
     * <li> Store the hull points in the root
     * <li> Add children with start and count data
     * <li> Recursively move to children
     * </ul>
     */
    buildHullTree(root) {
        AlternatingCCTreeNode.createWithIndices(this.indexOfMaxX, this.period + 1, root);
        return this.buildHullTreeGo(root, true);
    }
}
export class AlternatingCCTreeNodeCurveClipper {
    _curve;
    _intervalStack;
    _stackDepth;
    constructor() {
        this._stackDepth = 0;
        this._intervalStack = [];
    }
    setCurveRef(curve) { this._curve = curve; }
    popSegmentFrame() {
        if (this._stackDepth > 0) {
            this._topOfStack.length = 0; // formality.
            this._stackDepth -= 1;
        }
    }
    clearSegmentStack() {
        while (this._stackDepth > 0)
            this.popSegmentFrame(); // and that will reduce stack depth
    }
    pushEmptySegmentFrame() {
        this._stackDepth += 1;
        while (this._intervalStack.length < this._stackDepth)
            this._intervalStack.push([]);
        this._topOfStack.length = 0;
    }
    get _topOfStack() { return this._intervalStack[this._stackDepth - 1]; }
    // set the top of the stack (as defined by stackDepth -- not array length)
    set _topOfStack(value) {
        const n = this._stackDepth;
        if (n > 0)
            this._intervalStack[n - 1] = value;
    }
    /** Access entry [topOfStack() - numSkip] */
    stackEntry(numSkip) {
        if (numSkip <= this._stackDepth)
            return this._intervalStack[this._stackDepth - 1 - numSkip];
        else
            return [];
    }
    isTopOfStackEmpty() {
        return this._topOfStack.length === 0;
    }
    // Is re-used by method calls
    static _fractionIntervals = [];
    appendSingleClipToStack(planes, insideSegments) {
        const fractionIntervals = AlternatingCCTreeNodeCurveClipper._fractionIntervals;
        if (this._curve instanceof LineSegment3d) {
            const segment = this._curve;
            let f0;
            let f1;
            if (segment.announceClipIntervals(planes, (a0, a1, _cp) => { f0 = a0; f1 = a1; })) {
                insideSegments.push(Range1d.createXX(f0, f1));
            }
            return true;
        }
        else if (this._curve instanceof Arc3d) {
            const arc = this._curve;
            fractionIntervals.length = 0;
            arc.announceClipIntervals(planes, (a0, a1, _cp) => {
                fractionIntervals.push(a0);
                fractionIntervals.push(a1);
            });
            for (let i = 0; i < fractionIntervals.length; i += 2)
                insideSegments.push(Range1d.createXX(fractionIntervals[i], fractionIntervals[i + 1]));
            return true;
        }
        else if (this._curve instanceof LineString3d && (this._curve).points.length > 1) {
            const linestring = this._curve;
            let f0;
            let f1;
            const nPoints = linestring.points.length;
            const df = 1.0 / (nPoints - 1);
            for (let i = 0; i < nPoints - 1; i++) {
                const segment = LineSegment3d.create(linestring.points[i], linestring.points[i + 1]);
                if (segment.announceClipIntervals(planes, (a0, a1, _cp) => { f0 = a0; f1 = a1; })) {
                    insideSegments.push(Range1d.createXX((i + f0) * df, (i + f1) * df));
                }
            }
            return true;
        }
        else if (this._curve instanceof BSplineCurve3d) {
            const bcurve = this._curve;
            fractionIntervals.length = 0;
            bcurve.announceClipIntervals(planes, (a0, a1, _cp) => {
                fractionIntervals.push(a0);
                fractionIntervals.push(a1);
            });
            for (let i = 0; i < fractionIntervals.length; i += 2)
                insideSegments.push(Range1d.createXX(fractionIntervals[i], fractionIntervals[i + 1]));
            return true;
        }
        return false;
    }
    /**
     * Run one level of recursion. On return, the stack is one level deeper than at entry and the new top of the stack
     * has clip for this node (expensive -- must clone items of arrays during "swaps").
     */
    recurse(node) {
        this.pushEmptySegmentFrame();
        this.appendSingleClipToStack(node.planes, this._topOfStack);
        Range1dArray.sort(this._topOfStack);
        if (this.isTopOfStackEmpty())
            return;
        for (const child of node.children) {
            this.recurse(child);
            if (!this.isTopOfStackEmpty()) {
                const ranges = Range1dArray.differenceSorted(this.stackEntry(1), this.stackEntry(0));
                this.popSegmentFrame();
                this._topOfStack = ranges;
            }
            else {
                this.popSegmentFrame();
            }
            if (this.isTopOfStackEmpty())
                break;
        }
    }
    /**
     * Modifies the insideIntervals array given in place.
     * Note: curve given is passed by reference and stored.
     */
    appendSingleClipPrimitive(root, curve, insideIntervals, _outsideIntervals) {
        this.setCurveRef(curve);
        this.clearSegmentStack();
        this.recurse(root);
        if (this._stackDepth !== 1)
            return;
        const intervals = this._topOfStack;
        for (const interval of intervals) {
            const f0 = interval.low;
            const f1 = interval.high;
            const xyz0 = curve.fractionToPoint(f0);
            const xyz1 = curve.fractionToPoint(f1);
            insideIntervals.push(CurveLocationDetailPair.createCapture(CurveLocationDetail.createCurveFractionPoint(curve, f0, xyz0), CurveLocationDetail.createCurveFractionPoint(curve, f1, xyz1)));
        }
        this.popSegmentFrame();
    }
    /**
     * Modifies the insideIntervals array given in place.
     * Note: curve given is passed by reference and stored.
     */
    appendCurveCollectionClip(root, curve, insideIntervals, outsideIntervals) {
        for (const cp of curve.children) {
            if (cp instanceof CurvePrimitive)
                this.appendSingleClipPrimitive(root, cp, insideIntervals, outsideIntervals);
            else if (cp instanceof CurveCollection)
                this.appendCurveCollectionClip(root, cp, insideIntervals, outsideIntervals);
        }
    }
}
//# sourceMappingURL=AlternatingConvexClipTree.js.map