/*---------------------------------------------------------------------------------------------
* 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 { CurveChain } from "../curve/CurveCollection";
import { CurvePrimitive } from "../curve/CurvePrimitive";
import { LineString3d } from "../curve/LineString3d";
import { Loop } from "../curve/Loop";
import { ParityRegion } from "../curve/ParityRegion";
import { RegionOps } from "../curve/RegionOps";
import { UnionRegion } from "../curve/UnionRegion";
import { IndexedReadWriteXYZCollection } from "./IndexedXYZCollection";
import { Point3d } from "./Point3dVector3d";
import { PolygonOps } from "./PolygonOps";
import { Ray3d } from "./Ray3d";
/** Abstract base class for area-related queries of an xy-loop.
 * * Subclasses have particular logic for `Loop` and polygon data.
 * @internal
 */
class SimpleRegionCarrier {
    /** Fractions for interior point search. */
    searchFractions = [0.2349, 0.4142, 0.6587, 0.8193];
    /**
     * Given a region boundary tangent, construct a point interior to the region.
     * @param ray point and tangent on an edge of the region (modified on return)
     */
    constructInteriorPoint(ray) {
        ray.direction.z = 0.0;
        if (!ray.direction.normalizeInPlace())
            return undefined; // loop has zero length edge, or a vertical (gap) edge
        ray.direction.rotate90CCWXY(ray.direction);
        if (this.signedArea < 0.0)
            ray.direction.scaleInPlace(-1.0); // aim toward the region interior
        const refDistance = Math.sqrt(Math.abs(this.signedArea));
        const candidatePoint = Point3d.create();
        for (let fraction = 1.0e-5; fraction < 3; fraction *= 5.0) {
            ray.fractionToPoint(fraction * refDistance, candidatePoint);
            if (1 === this.classifyPointXY(candidatePoint))
                return candidatePoint;
        }
        return undefined;
    }
}
/**
 * Implement `SimpleRegionCarrier` queries with the area as a polygon carried in an `IndexedReadWriteXYZCollection`.
 */
class PolygonCarrier extends SimpleRegionCarrier {
    data;
    _signedArea;
    get signedArea() { return this._signedArea; }
    constructor(data) {
        super();
        this.data = data;
        this._signedArea = PolygonOps.areaXY(data);
    }
    /**
     * classify xy parts of point wrt this loop.
     * @param xy
     * @internal
     */
    classifyPointXY(xy) {
        return PolygonOps.classifyPointInPolygonXY(xy.x, xy.y, this.data);
    }
    /** Return some point "inside". */
    getAnyInteriorPoint() {
        for (let childIndex = 0; childIndex < this.data.length; childIndex++) {
            for (const fraction of this.searchFractions) {
                const q = this.constructInteriorPointNearEdge(childIndex, fraction);
                if (q !== undefined)
                    return q;
            }
        }
        return undefined;
    }
    grabPolygon() { return this.data; }
    grabLoop() {
        return Loop.createPolygon(this.data);
    }
    reverseForAreaSign(targetSign) {
        if (targetSign * this._signedArea < 0.0) {
            this.data.reverseInPlace();
            this._signedArea *= -1.0;
        }
    }
    constructInteriorPointNearEdge(edgeIndex, fractionAlong) {
        if (edgeIndex + 1 < this.data.length) {
            const ray = Ray3d.createCapture(this.data.interpolateIndexIndex(edgeIndex, fractionAlong, edgeIndex + 1), this.data.vectorIndexIndex(edgeIndex, edgeIndex + 1));
            return this.constructInteriorPoint(ray);
        }
        return undefined;
    }
}
/**
 * Implement `SimpleRegionCarrier` queries with the area as a strongly typed `Loop`.
 */
class LoopCarrier extends SimpleRegionCarrier {
    data;
    _signedArea;
    get signedArea() { return this._signedArea; }
    constructor(data) {
        super();
        this.data = data;
        const areaMoments = RegionOps.computeXYAreaMoments(data);
        this._signedArea = areaMoments !== undefined ? areaMoments.quantitySum : 0.0;
    }
    /**
     * classify xy parts of point wrt this loop.
     * @param xy
     * @internal
     */
    classifyPointXY(xy) {
        return RegionOps.testPointInOnOutRegionXY(this.data, xy.x, xy.y);
    }
    constructInteriorPointNearChild(childIndex, fractionAlong) {
        if (childIndex < this.data.children.length) {
            const ray = this.data.children[childIndex].fractionToPointAndDerivative(fractionAlong);
            return this.constructInteriorPoint(ray);
        }
        return undefined;
    }
    /** Return some point "inside". */
    getAnyInteriorPoint() {
        for (let childIndex = 0; childIndex < this.data.children.length; childIndex++) {
            for (const fraction of this.searchFractions) {
                const q = this.constructInteriorPointNearChild(childIndex, fraction);
                if (q !== undefined)
                    return q;
            }
        }
        return undefined;
    }
    grabPolygon() {
        const strokes = this.data.cloneStroked();
        if (strokes instanceof CurveChain) {
            const linestring = LineString3d.create();
            for (const child of strokes.children) {
                if (child instanceof CurvePrimitive) {
                    child.emitStrokes(linestring);
                }
            }
            return linestring.numPoints() > 0 ? linestring.packedPoints : undefined;
        }
        return undefined;
    }
    grabLoop() {
        return this.data;
    }
    reverseForAreaSign(targetSign) {
        if (targetSign * this._signedArea < 0.0) {
            this.data.reverseChildrenInPlace();
            this._signedArea *= -1.0;
        }
    }
}
/**
 * A `SortablePolygon` carries a (single) xy-loop with data useful for sorting for inner-outer structure.
 * @internal
 */
export class SortablePolygon {
    _loopCarrier;
    anyPoint;
    sortKey;
    range;
    parentIndex;
    isHole;
    outputSetIndex;
    /**
     *
     * @param loop Loop to capture.
     */
    constructor(loop, range) {
        if (loop instanceof IndexedReadWriteXYZCollection)
            this._loopCarrier = new PolygonCarrier(loop);
        else
            this._loopCarrier = new LoopCarrier(loop);
        this.range = range;
        this.sortKey = Math.abs(this._loopCarrier.signedArea);
        this.isHole = false;
    }
    /** Push loop with sort data onto the array.
     * * No action if no clear normal.
     * * return true if pushed.
     */
    static pushPolygon(loops, loop) {
        const range = loop.getRange();
        const sortablePolygon = new SortablePolygon(loop, range);
        if (sortablePolygon.sortKey > 0.0) {
            loops.push(sortablePolygon);
            return true;
        }
        return false;
    }
    /** Push loop with sort data onto the array.
     * * No action if no clear normal.
     * * return true if pushed.
     */
    static pushLoop(loops, loop) {
        const range = loop.range();
        const sortablePolygon = new SortablePolygon(loop, range);
        if (sortablePolygon.sortKey > 0.0) {
            loops.push(sortablePolygon);
            return true;
        }
        return false;
    }
    /** Push loop with sort data onto the array.
     * * No action if no clear normal.
     * * return true if pushed.
     */
    static assignParentsAndDepth(loops) {
        // Sort largest to smallest ...
        loops.sort((loopA, loopB) => (loopB.sortKey - loopA.sortKey));
        // starting with smallest loop, point each loop to smallest containing parent.
        for (let i = loops.length; i-- > 0;) {
            const thisLoop = loops[i];
            const xy = thisLoop._loopCarrier.getAnyInteriorPoint();
            if (xy !== undefined) {
                // find smallest containing parent (search forward only to hit)
                loops[i].parentIndex = undefined;
                loops[i].outputSetIndex = undefined;
                for (let j = i; j-- > 0;) {
                    const otherLoop = loops[j];
                    if (otherLoop.range.containsXY(xy.x, xy.y)) {
                        if (1 === otherLoop._loopCarrier.classifyPointXY(xy)) {
                            thisLoop.parentIndex = j;
                            // The loops are searched from small area to larger.  Any other containing loop is larger, so otherLoop must be the smallest.
                            break;
                        }
                    }
                }
            }
        }
    }
    static assemblePolygonSet(loops) {
        const outputSets = [];
        // In large-to-small order:
        // If a loop has no parent or has a "hole" as parent it is outer.
        // otherwise (i.e. it has a non-hole parent) it becomes a hole in the parent.
        for (const loopData of loops) {
            loopData.isHole = false;
            const parentIndex = loopData.parentIndex;
            if (parentIndex !== undefined)
                loopData.isHole = !loops[parentIndex].isHole;
            if (!loopData.isHole) {
                loopData._loopCarrier.reverseForAreaSign(1.0);
                loopData.outputSetIndex = outputSets.length;
                outputSets.push([]);
                outputSets[loopData.outputSetIndex].push(loopData._loopCarrier.grabPolygon());
            }
            else {
                loopData._loopCarrier.reverseForAreaSign(-1.0);
                const outputSetIndex = loops[parentIndex].outputSetIndex;
                outputSets[outputSetIndex].push(loopData._loopCarrier.grabPolygon());
            }
        }
        return outputSets;
    }
    static assembleLoopSet(loops) {
        const outputSets = [];
        const numLoops = loops.length;
        // In large-to-small order:
        // If a loop has no parent or has a "hole" as parent it is outer.
        // otherwise (i.e. it has a non-hole parent) it becomes a hole in the parent.
        for (let candidateIndex = 0; candidateIndex < numLoops; candidateIndex++) {
            const candidateData = loops[candidateIndex];
            const parentIndex = candidateData.parentIndex;
            candidateData.isHole = parentIndex !== undefined ? !loops[parentIndex].isHole : false;
            if (!candidateData.isHole) {
                candidateData._loopCarrier.reverseForAreaSign(1.0);
                const candidateLoop = candidateData._loopCarrier.grabLoop();
                let candidateParityRegion;
                // find all directly contained children . . .
                for (let childIndex = candidateIndex + 1; childIndex < numLoops; childIndex++) {
                    const childData = loops[childIndex];
                    if (childData.parentIndex === candidateIndex) {
                        if (candidateParityRegion === undefined) {
                            candidateParityRegion = ParityRegion.create();
                            candidateParityRegion.tryAddChild(candidateLoop);
                            childData._loopCarrier.reverseForAreaSign(-1.0);
                            candidateParityRegion.tryAddChild(childData._loopCarrier.grabLoop());
                        }
                        else {
                            childData._loopCarrier.reverseForAreaSign(-1.0);
                            candidateParityRegion.tryAddChild(childData._loopCarrier.grabLoop());
                        }
                    }
                }
                if (candidateParityRegion !== undefined)
                    outputSets.push(candidateParityRegion);
                else if (candidateLoop !== undefined)
                    outputSets.push(candidateLoop);
            }
        }
        return outputSets;
    }
    static sortAsAnyRegion(loops) {
        this.assignParentsAndDepth(loops);
        const regions = this.assembleLoopSet(loops);
        if (regions.length === 1)
            return regions[0];
        else {
            const unionRegion = UnionRegion.create();
            for (const region of regions)
                unionRegion.tryAddChild(region);
            return unionRegion;
        }
    }
    static sortAsArrayOfArrayOfPolygons(loops) {
        this.assignParentsAndDepth(loops);
        return this.assemblePolygonSet(loops);
    }
    grabPolygon() {
        return this._loopCarrier.grabPolygon();
    }
    grabLoop() {
        return this._loopCarrier.grabLoop();
    }
    reverseForAreaSign(targetSign) {
        this._loopCarrier.reverseForAreaSign(targetSign);
    }
    getAnyInteriorPoint() {
        return this._loopCarrier.getAnyInteriorPoint();
    }
}
//# sourceMappingURL=SortablePolygon.js.map