/*---------------------------------------------------------------------------------------------
* Copyright (c) Bentley Systems, Incorporated. All rights reserved.
* See LICENSE.md in the project root for license terms and full copyright notice.
*--------------------------------------------------------------------------------------------*/
import { assert, OrderedSet, SortedArray } from "@itwin/core-bentley";
import { Geometry } from "../../Geometry";
import { Angle } from "../../geometry3d/Angle";
import { AngleSweep } from "../../geometry3d/AngleSweep";
import { Point3d, Vector3d } from "../../geometry3d/Point3dVector3d";
import { Range1d } from "../../geometry3d/Range";
import { Ray3d } from "../../geometry3d/Ray3d";
import { Transform } from "../../geometry3d/Transform";
import { Arc3d, EllipticalArcApproximationOptions, EllipticalArcSampleMethod } from "../Arc3d";
import { Loop } from "../Loop";
import { Path } from "../Path";
import { CurveCurveCloseApproachXY } from "./CurveCurveCloseApproachXY";
/** @packageDocumentation
 * @module Curve
 */
/**
 * Comparison callback to sort fractions in increasing order with suitable tolerance for equality.
 * @internal
 */
function compareFractionsIncreasing(f0, f1) {
    if (Geometry.isAlmostEqualNumber(f0, f1, Geometry.smallFraction))
        return 0;
    return f0 < f1 ? -1 : 1;
}
;
/**
 * Comparison callback to sort fractions in decreasing order with suitable tolerance for equality.
 * @internal
 */
function compareFractionsDecreasing(f0, f1) {
    if (Geometry.isAlmostEqualNumber(f0, f1, Geometry.smallFraction))
        return 0;
    return f0 < f1 ? 1 : -1;
}
;
/**
 * Structured data carrier used by the elliptical arc sampler.
 * @internal
*/
export class QuadrantFractions {
    _quadrant;
    _fractions;
    _interpolateStartTangent;
    _interpolateEndTangent;
    _averageAdded;
    constructor(quadrant, fractions, interpolateStartTangent, interpolateEndTangent) {
        this._quadrant = quadrant;
        this._fractions = fractions;
        this._interpolateStartTangent = interpolateStartTangent;
        this._interpolateEndTangent = interpolateEndTangent;
        this._averageAdded = false;
    }
    /** Constructor, captures the array. */
    static create(quadrant, fractions = [], interpolateStartTangent = true, interpolateEndTangent = true) {
        return new QuadrantFractions(quadrant, fractions, interpolateStartTangent, interpolateEndTangent);
    }
    /**
     * Quadrant of the full ellipse containing the samples.
     * * Quadrants are labeled proceeding in counterclockwise angular sweeps of length pi/2 starting at vector0.
     * * For example, Quadrant 1 starts at vector0 and ends at vector90, and Quadrant 4 ends at vector0.
     * * For purposes of angle classification, quadrants are half-open intervals, closed at their start angle,
     * as determined by the ellipse's sweep direction.
     */
    get quadrant() {
        return this._quadrant;
    }
    /** Sample locations in this quadrant of the elliptical arc, as fractions of its sweep. */
    get fractions() {
        return this._fractions;
    }
    set fractions(f) {
        this._fractions = f;
    }
    /**
     * Whether to interpolate the elliptical arc tangent at the first fraction.
     * * If true (default), the first approximating arc is computed from the first two fractions and approximates the
     * elliptical arc between the first and second fractions, interpolating both point and tangent at the first fraction,
     * and point at the second fraction.
     * * If false, the first approximating arc is computed from the first three fractions and approximates the
     * elliptical arc between the second and third fractions, where it interpolates position only.
     */
    get interpolateStartTangent() {
        return this._interpolateStartTangent;
    }
    set interpolateStartTangent(interpolate) {
        this._interpolateStartTangent = interpolate;
    }
    /**
     * Whether to interpolate the elliptical arc tangent at the last fraction.
     * * If true (default), the last approximating arc is computed from the last two fractions and approximates the
     * elliptical arc between the penultimate and last fractions, interpolating point at the penultimate fraction, and
     * both point and tangent at the last fraction.
     * * If false, the last approximating arc is computed from the last three fractions and approximates the
     * elliptical arc between the penultimate and last fractions, where it interpolates position only.
     */
    get interpolateEndTangent() {
        return this._interpolateEndTangent;
    }
    set interpolateEndTangent(interpolate) {
        this._interpolateEndTangent = interpolate;
    }
    /**
     * Whether the average of the first and last fractions was added to satisfy a minimum fractions array length of three.
     * * There are always at least two fractions per quadrant, but three are needed to interpolate both end tangents
     * with circular arcs.
     * * This flag is set if a given sample method/arc yielded only two fractions, so their average was inserted in the
     * fractions array to meet this minimum three-sample requirement.
    */
    get averageAdded() {
        return this._fractions.length === 3 ? this._averageAdded : false;
    }
    set averageAdded(added) {
        this._averageAdded = added;
    }
    /**
     * Compute quadrant data for the given angles.
     * @param radians0 first radian angle.
     * @param radians1 second radian angle.
     * @return quadrant number and start/end radian angles for the quadrant that contains both input angles, or
     * `undefined` if no such quadrant.
     * * The returned sweep is always counterclockwise: angle0 < angle1.
     */
    static getQuadrantRadians(radians0, radians1) {
        if (AngleSweep.isRadiansInStartEnd(radians0, 0, Angle.piOver2Radians)
            && AngleSweep.isRadiansInStartEnd(radians1, 0, Angle.piOver2Radians))
            return { quadrant: 1, angle0: 0, angle1: Angle.piOver2Radians };
        if (AngleSweep.isRadiansInStartEnd(radians0, Angle.piOver2Radians, Angle.piRadians)
            && AngleSweep.isRadiansInStartEnd(radians1, Angle.piOver2Radians, Angle.piRadians))
            return { quadrant: 2, angle0: Angle.piOver2Radians, angle1: Angle.piRadians };
        if (AngleSweep.isRadiansInStartEnd(radians0, Angle.piRadians, Angle.pi3Over2Radians)
            && AngleSweep.isRadiansInStartEnd(radians1, Angle.piRadians, Angle.pi3Over2Radians))
            return { quadrant: 3, angle0: Angle.piRadians, angle1: Angle.pi3Over2Radians };
        if (AngleSweep.isRadiansInStartEnd(radians0, Angle.pi3Over2Radians, Angle.pi2Radians)
            && AngleSweep.isRadiansInStartEnd(radians1, Angle.pi3Over2Radians, Angle.pi2Radians))
            return { quadrant: 4, angle0: Angle.pi3Over2Radians, angle1: Angle.pi2Radians };
        return undefined;
    }
    /** Compute the fractional range of Quadrant 1 for the given full sweep. */
    static getQ1FractionalRange(fullSweep) {
        const angle0 = 0;
        const angle90 = 0.5 * Math.PI;
        let f0 = fullSweep.radiansToSignedPeriodicFraction(angle0);
        let f1 = fullSweep.radiansToSignedPeriodicFraction(angle90);
        if (!fullSweep.isCCW)
            [f0, f1] = [f1, f0];
        if (f1 < f0)
            f1 += 1;
        return Range1d.createXX(f0, f1);
    }
    /** Reverse the fractions array and flags. */
    reverse() {
        this._fractions.reverse();
        [this._interpolateStartTangent, this._interpolateEndTangent] = [this._interpolateEndTangent, this._interpolateStartTangent];
    }
}
;
/**
 * Base class specifying callbacks for processing samples of an elliptical arc.
 * @internal
 */
class QuadrantFractionsProcessor {
    /**
     * Announce the beginning of processing for quadrant `q`.
     * @param _reversed whether `q.reverse()` was invoked before this call for symmetry reasons. If so, arcs will be
     * announced in the opposite order and with the opposite orientation.
     * @return whether to process `q`.
     */
    announceQuadrantBegin(_q, _reversed) { return true; }
    /**
     * Announce a circular arc approximating the elliptical arc E between the given fractions.
     * * The given fractions are different. If `announceQuadrantBegin` was invoked with `reversed === false` then
     * `fPrev < f0 < f1`; otherwise, `fPrev > f0 > f1`.
     * @param _arc circular arc that interpolates E at f0 and f1. Processor can capture `arc`; it is unused afterwards.
     * @param _fPrev fractional parameter of E used to define the 3-point parent circle through the points at `fPrev`,
     * `f0`, and `f1` from which `arc` was constructed. If undefined, `arc` was generated from the circle defined by
     * the points at `f0` and `f1` and one of their tangents.
     * @param _f0 fractional parameter of E at which point `arc` starts.
     * @param _f1 fractional parameter of E at which point `arc` ends.
     */
    announceArc(_arc, _fPrev, _f0, _f1) { }
    /**
     * Announce the end of processing for quadrant `q`.
     * @param _reversed whether `q.reverse()` was invoked before processing (see [[announceQuadrantBegin]]). If so,
     * after this call `q.reverse()` is invoked again.
     */
    announceQuadrantEnd(_q, _reversed) { }
}
;
/**
 * Processor for computing the error of a sample-based arc chain approximation.
 * @internal
 */
class ArcChainErrorProcessor extends QuadrantFractionsProcessor {
    _ellipticalArc;
    _maxPerpendicular;
    constructor(ellipticalArc) {
        super();
        this._ellipticalArc = ellipticalArc;
        this._maxPerpendicular = undefined;
    }
    static create(ellipticalArc) {
        return new ArcChainErrorProcessor(ellipticalArc);
    }
    get ellipticalArc() {
        return this._ellipticalArc;
    }
    /**
     * Compute the maximum xy-distance between an elliptical arc and its approximation.
     * * Inputs should be in horizontal plane(s), as z-coordinates are ignored.
     * @param circularArc circular arc approximant. Assumed to start and end on the elliptical arc.
     * @param ellipticalArc elliptical arc being approximated.
     * For best results, `f0` and `f1` should correspond to the start/end of `circularArc`.
     * @param f0 optional `ellipticalArc` start fraction to restrict its sweep.
     * @param f1 optional `ellipticalArc` end fraction to restrict its sweep.
     * @return details of the perpendicular measuring the max approximation error, or undefined if no such perpendicular.
     * For each of `detailA` (refers to `circularArc`) and `detailB` (refers to unrestricted `ellipticalArc`):
     * * `point` is the end of the perpendicular on each curve.
     * * `fraction` is the curve parameter of the point.
     * * `a` is the distance between the points.
     */
    static computePrimitiveErrorXY(circularArc, ellipticalArc, f0, f1) {
        const handler = new CurveCurveCloseApproachXY();
        handler.maxDistanceToAccept = circularArc.quickLength() / 2;
        const trimEllipse = undefined !== f0 && undefined !== f1;
        const trimmedEllipticalArc = trimEllipse ? ellipticalArc.clonePartialCurve(f0, f1) : ellipticalArc;
        // we expect only one perpendicular, not near an endpoint.
        handler.allPerpendicularsArcArcBounded(circularArc, trimmedEllipticalArc);
        let maxPerp;
        for (const perp of handler.grabPairedResults()) {
            if (Geometry.isAlmostEqualEitherNumber(perp.detailA.fraction, 0, 1, Geometry.smallFraction))
                continue; // rule out perpendiculars on circular arc ends
            if (Geometry.isAlmostEqualEitherNumber(perp.detailB.fraction, 0, 1, Geometry.smallFraction))
                continue; // rule out perpendiculars on elliptical arc ends
            const error = perp.detailA.point.distanceXY(perp.detailB.point);
            if (!maxPerp || maxPerp.detailA.a < error) {
                if (trimEllipse) { // reset ellipticalArc fraction to unrestricted range
                    perp.detailB.fraction = Geometry.interpolate(f0, perp.detailB.fraction, f1);
                    perp.detailB.setCurve(ellipticalArc);
                }
                perp.detailA.a = perp.detailB.a = error;
                maxPerp = perp;
            }
        }
        return maxPerp;
    }
    get maxPerpendicular() {
        return this._maxPerpendicular;
    }
    set maxPerpendicular(newMaxPerp) {
        this._maxPerpendicular = newMaxPerp;
    }
    /**
     * Update the chain approximation error for a given chain child that approximates the elliptical arc between the
     * given fractions.
     * * Fractional sweep [f0, f1] of the elliptical arc is the smaller of the cyclic sweeps.
     */
    updateMaxPerpendicular(childApproximation, f0, f1) {
        const childPerp = ArcChainErrorProcessor.computePrimitiveErrorXY(childApproximation, this.ellipticalArc, f0, f1);
        if (childPerp && (!this.maxPerpendicular || this.maxPerpendicular.detailA.a < childPerp.detailA.a))
            this.maxPerpendicular = childPerp;
    }
    ;
    announceArc(arc, _fPrev, f0, f1) {
        this.updateMaxPerpendicular(arc, f0, f1);
    }
}
/**
 * Processor for computing an optimal refinement of a single Q1 ordered interval [f0,f1] by perturbing an interior
 * seed fraction f.
 * * Typically the caller (cf. [[AdaptiveSubdivisionQ1ErrorProcessor.announceArc]] processes a
 * QuadrantFractions `q` with `q.quadrant` = 1 until convergence. The `q.fractions` array is expected to have
 * one of two forms:
 *   * [fPrev, f0, f, f1], where fPrev is from the adjacent interval (possibly just refined), or
 *   * [f0, f, f1], if `q.interpolateStartTangent === true`.
 * * This processor implements a bisection algorithm that iteratively shrinks a sub-interval that brackets f,
 * starting with [f0,f1]:
 *   * During processing, `announceArc` will be called twice to compute the approximation errors of the circular
 * arcs on either side of f in the current bracket.
 *   * In `announceQuadrantEnd` if these two errors are almost equal, we are done refining [f0,f1]. Otherwise,
 * we move f halfway to the endpoint of the bracket that decreases the error delta, and shrink our bracket.
 * @internal
 */
class AdaptiveSubdivisionQ1IntervalErrorProcessor extends QuadrantFractionsProcessor {
    _fullEllipseXY;
    _f;
    _bracket0;
    _bracket1;
    _error0;
    _error1;
    constructor(fullEllipseXY, f0, f, f1) {
        super();
        this._fullEllipseXY = fullEllipseXY;
        this._bracket0 = f0;
        this._f = f;
        this._bracket1 = f1;
        this._error0 = this._error1 = Geometry.largeCoordinateResult;
    }
    static create(fullEllipseXY, f0, f, f1) {
        return new AdaptiveSubdivisionQ1IntervalErrorProcessor(fullEllipseXY, f0, f, f1);
    }
    /**
     * The arc to approximate, transformed to local coordinates, and with full sweep.
     * * Local coordinates allows us to ignore z in determining approximation error.
     * * Full sweep guarantees we have the first quadrant in which to do our computations.
     */
    get fullEllipseXY() {
        return this._fullEllipseXY;
    }
    get f() {
        return this._f;
    }
    get isConverged() {
        if (Geometry.isSmallMetricDistance(this._error0 - this._error1))
            return true;
        if (Geometry.isSmallRelative(this._bracket0 - this._bracket1))
            return true;
        return false;
    }
    /** Remember the initial value of the fraction f to be perturbed. */
    announceQuadrantBegin(q, reversed) {
        assert(q.quadrant === 1);
        assert(!reversed); // ASSUME [bracket0, bracket1] and q.fractions have the same ordering
        // the first fraction might be an extra point for computing the first 3-pt arc
        assert(q.fractions.length === 4 || (q.fractions.length === 3 && q.interpolateStartTangent));
        this._error0 = this._error1 = Geometry.largeCoordinateResult;
        return true;
    }
    /** Compute approximation error over the interval adjacent to f. */
    announceArc(arc, _fPrev, f0, f1) {
        if (Geometry.isAlmostEqualEitherNumber(this.f, f0, f1, 0)) {
            const perp = ArcChainErrorProcessor.computePrimitiveErrorXY(arc, this.fullEllipseXY, f0, f1);
            if (perp) {
                if (this.f === f1)
                    this._error0 = perp.detailA.a; // first arc error
                else // f === f0
                    this._error1 = perp.detailA.a; // second arc error
            }
        }
    }
    /** Update `q.fractions` with a perturbed value of f that is expected to decrease error delta. */
    announceQuadrantEnd(q, _reversed) {
        if (Geometry.isLargeCoordinateResult(this._error0) || Geometry.isLargeCoordinateResult(this._error1))
            return;
        if (this.isConverged)
            return;
        // set up for next call to processQuadrantFractions
        const n = q.fractions.length;
        if (this._error0 < this._error1)
            this._bracket0 = this._f; // HEURISTIC: move f toward f1 to decrease e1
        else
            this._bracket1 = this._f; // HEURISTIC: move f toward f0 to decrease e0
        this._f = q.fractions[n - 2] = Geometry.interpolate(this._bracket0, 0.5, this._bracket1);
    }
}
/**
 * Processor for computing samples in Q1 for a subdivision-based arc chain approximation.
 * * The basic idea is to build a refinement of `q.fractions` for a QuadrantFractions q with q.quadrant = 1.
 *   * In `announceQuadrantBegin` we start off the refinement with a copy of `q.fractions`.
 *   * In `announceArc` we are called to process the interval [f0,f1] in `q.fractions`. First we test if the
 * announced arc's approximation error over [f0,f1] exceeds maxError.
 *     * If so, we employ [[AdaptiveSubdivisionQ1IntervalErrorProcessor]] to compute an interior fraction f
 * that best refines the interval, which becomes [f0,f,f1].
 *     * Otherwise, the fraction span [f0,f1] is unchanged---no additional samples are needed to decrease the
 * approximation error.
 *   * In `announceQuadrantEnd`, `q.fractions` is updated in place with the computed refinement.
 *   * The caller (e.g., [[AdaptiveSubdivisionSampler.computeRadiansStrictlyInsideQuadrant1]]) typically
 * re-processes `q` until `isRefined` returns false, at which point construction of an approximation that is
 * guaranteed not to exceed the desired error can commence.
 * @internal
 */
class AdaptiveSubdivisionQ1ErrorProcessor extends QuadrantFractionsProcessor {
    _fullEllipseXY;
    _fractionRangeQ1;
    _refinement;
    _maxError;
    _originalRefinementCount;
    static _maxIters = 50;
    constructor(fullEllipseXY, maxError) {
        super();
        this._fullEllipseXY = fullEllipseXY;
        this._fractionRangeQ1 = QuadrantFractions.getQ1FractionalRange(fullEllipseXY.sweep);
        this._maxError = maxError > 0 ? maxError : EllipticalArcApproximationOptions.defaultMaxError;
        this._originalRefinementCount = 0;
    }
    static create(fullEllipseXY, maxError) {
        return new AdaptiveSubdivisionQ1ErrorProcessor(fullEllipseXY, maxError);
    }
    /**
     * The arc to approximate, transformed to local coordinates, and with full sweep.
     * * Local coordinates allows us to ignore z in determining approximation error.
     * * Full sweep guarantees we have the first quadrant in which to do our computations.
     */
    get fullEllipseXY() {
        return this._fullEllipseXY;
    }
    /** Whether the processor refined the current `QuadrantFractions` fractions array to decrease approximation error. */
    get isRefined() {
        if (undefined === this._refinement || 0 === this._refinement.length)
            return false;
        return this._originalRefinementCount < this._refinement.length;
    }
    /** Initialize the refinement from the quadrant fractions array. */
    announceQuadrantBegin(q, reversed) {
        assert(q.quadrant === 1);
        this._refinement = new SortedArray(reversed ? compareFractionsDecreasing : compareFractionsIncreasing, false);
        for (const f of q.fractions) {
            if (this._fractionRangeQ1.containsX(f))
                this._refinement.insert(f);
        }
        return 2 <= (this._originalRefinementCount = this._refinement.length);
    }
    /**
     * Return the adjacent fraction from the previously refined interval.
     * * This is used to refine the interval of an inner arc, which depends on the most recent refinement of the
     * previous interval.
     */
    getPreviousFraction(f0) {
        if (undefined === this._refinement)
            return undefined;
        const iPrev = this._refinement.indexOf(f0);
        return (iPrev >= 1) ? this._refinement.get(iPrev - 1) : undefined;
    }
    /** If this arc needs to be refined, add a refinement point. */
    announceArc(arc, fPrev, f0, f1) {
        if (undefined === this._refinement)
            return;
        if (this._originalRefinementCount > 2) { // no early out for a single interval; it gets refined below
            const perp = ArcChainErrorProcessor.computePrimitiveErrorXY(arc, this.fullEllipseXY, f0, f1);
            if (!perp || perp.detailA.a <= this._maxError)
                return;
        }
        // throughout this function, f0 and f1 may be in either order
        const f = Geometry.interpolate(f0, 0.5, f1);
        const interpolateStartTangent = Geometry.isAlmostEqualEitherNumber(f0, this._fractionRangeQ1.low, this._fractionRangeQ1.high, 0);
        const interpolateEndTangent = Geometry.isAlmostEqualEitherNumber(f1, this._fractionRangeQ1.low, this._fractionRangeQ1.high, 0);
        if (!interpolateStartTangent && undefined === fPrev)
            fPrev = this.getPreviousFraction(f0); // createLastArc doesn't supply fPrev to announceArc
        const fractions = (undefined === fPrev) ? [f0, f, f1] : [fPrev, f0, f, f1];
        const q1 = [QuadrantFractions.create(1, fractions, interpolateStartTangent, interpolateEndTangent)];
        const processor = AdaptiveSubdivisionQ1IntervalErrorProcessor.create(this.fullEllipseXY, f0, f, f1);
        let iter = 0;
        do { // bisect to refine f (starting at avg) to balance the approx error of the arcs on either side
            EllipticalArcApproximationContext.processQuadrantFractions(this.fullEllipseXY, q1, processor);
        } while (iter++ < AdaptiveSubdivisionQ1ErrorProcessor._maxIters && !processor.isConverged);
        this._refinement.insert(processor.f);
    }
    /** Update the quadrant fractions array with the current refinement. */
    announceQuadrantEnd(q, _reversed) {
        if (this._refinement)
            q.fractions = [...this._refinement];
    }
    /**
     * Compute radian angles for the fractions in the current refinement that are strictly inside Q1.
     * @param result optional preallocated array to clear and populate.
     * @return angles suitable for output from [[EllipticalArcSampler.computeRadiansStrictlyInsideQuadrant1]].
     */
    getRefinedInteriorQ1Angles(result) {
        if (!result)
            result = [];
        else
            result.length = 0;
        if (this._refinement) {
            for (const f of this._refinement) {
                if (this._fractionRangeQ1.containsXOpen(f))
                    result.push(this.fullEllipseXY.sweep.fractionToRadians(f));
            }
        }
        return result;
    }
}
;
/**
 * Implementation for method `EllipticalArcSampleMethod.UniformParameter`
 * @internal
 */
class UniformParameterSampler {
    _context;
    _options;
    constructor(c, o) {
        this._context = c;
        this._options = o;
    }
    static create(context, options) {
        return new UniformParameterSampler(context, options);
    }
    computeRadiansStrictlyInsideQuadrant1(result) {
        if (!result)
            result = [];
        if (this._context.isValidEllipticalArc) {
            const aDelta = Angle.piOver2Radians / (this._options.numSamplesInQuadrant - 1);
            for (let i = 1; i < this._options.numSamplesInQuadrant - 1; ++i)
                result.push(i * aDelta);
        }
        return result;
    }
}
;
/**
 * Implementation for method `EllipticalArcSampleMethod.NonUniformCurvature`
 * @internal
 */
class NonUniformCurvatureSampler {
    _context;
    _options;
    _xMag2;
    _yMag2;
    _curvatureRange;
    constructor(c, o) {
        this._context = c;
        this._options = o;
        this._xMag2 = c.ellipticalArc.matrixRef.columnXMagnitudeSquared();
        this._yMag2 = c.ellipticalArc.matrixRef.columnYMagnitudeSquared();
        // extreme curvatures occur at the ellipse's axis points because its axes are perpendicular
        this._curvatureRange = Range1d.createXX(Math.sqrt(this._xMag2) / this._yMag2, Math.sqrt(this._yMag2) / this._xMag2);
    }
    static create(context, options) {
        return new NonUniformCurvatureSampler(context, options);
    }
    /**
     * Compute the angle corresponding to the point in the ellipse's first quadrant with the given curvature.
     * * The elliptical arc is assumed to be non-circular and have perpendicular axes of positive length; its sweep is ignored.
     * * This is a scaled inverse of [[Arc3d.fractionToCurvature]] restricted to fractions in [0, 1/4].
     * @return radian angle in [0, pi/2] or undefined if the ellipse is invalid, or does not attain the given curvature.
     */
    curvatureToRadians(curvature) {
        /*
        Let the elliptical arc be parameterized with axes u,v of different length and u.v = 0:
          f(t) = c + u cos(t) + v sin(t),
          f'(t) = -u sin(t) + v cos(t),
          f"(t) = -u cos(t) - v sin(t)
        We seek a formula for t(K), the inverse of the standard curvature formula
          K(t) := ||f'(t) x f"(t)|| / ||f'(t)||^3
        for a parametric function f(t):R->R^3. We'll restrict K to Q1 (i.e., t in [0, pi/2]), where K is monotonic.
        By linearity of the cross product and the above formulas, the numerator of K(t) reduces to ||u x v||, and so:
          cbrt(||u x v||/K) = ||f'(t)|| = sqrt(f'(t).f'(t))
        Leveraging u,v perpendicularity we can define:
          lambda(K) := (||u x v||/K)^(2/3) = (||u|| ||v|| / K)^(2/3) = cbrt(u.u v.v / K^2)
        Then substituting and using perpendicularity again:
          lambda(K) = f'(t).f'(t)
            = sin^2(t)u.u + cos^2(t)v.v - 2sin(t)cos(t)u.v
            = u.u + cos^2(t)(v.v - u.u)
        Taking the positive root because cos(t)>=0 in Q1, and relying on u,v having different lengths:
          cos(t) = sqrt((lambda(K) - u.u)/(v.v - u.u))
        Solving for t yields the formula for t(K).
        */
        if (!this._curvatureRange.containsX(curvature))
            return undefined; // ellipse does not attain this curvature
        const lambda = Math.cbrt((this._xMag2 * this._yMag2) / (curvature * curvature));
        const cosTheta = Math.sqrt(Math.abs((lambda - this._xMag2) / (this._yMag2 - this._xMag2)));
        return Math.acos(cosTheta);
    }
    computeRadiansStrictlyInsideQuadrant1(result) {
        if (!result)
            result = [];
        if (this._context.isValidEllipticalArc) {
            const tDelta = 1.0 / (this._options.numSamplesInQuadrant - 1);
            for (let i = 1; i < this._options.numSamplesInQuadrant - 1; ++i) {
                const j = this._options.remapFunction(i * tDelta);
                const curvature = (1 - j) * this._curvatureRange.low + j * this._curvatureRange.high;
                const angle = this.curvatureToRadians(curvature);
                if (undefined !== angle)
                    result.push(angle);
            }
        }
        return result;
    }
}
;
/**
 * Implementation for method `EllipticalArcSampleMethod.UniformCurvature`.
 * * Basically this is just `NonUniformCurvature` method with uniformity preserved via identity remap function.
 * @internal
 */
class UniformCurvatureSampler extends NonUniformCurvatureSampler {
    constructor(c, o) {
        super(c, o.clone());
        this._options.remapFunction = (x) => x; // identity map
    }
    static create(context, options) {
        return new UniformCurvatureSampler(context, options);
    }
}
;
/**
 * Implementation for method `EllipticalArcSampleMethod.AdaptiveSubdivision`
 * @internal
 */
class AdaptiveSubdivisionSampler {
    _context;
    _options;
    _fullEllipseXY;
    constructor(c, o) {
        this._context = c;
        this._options = o;
        this._fullEllipseXY = c.cloneLocalArc(true) ?? Arc3d.createUnitCircle();
    }
    static create(context, options) {
        return new AdaptiveSubdivisionSampler(context, options);
    }
    /**
     * Return a copy of the arc to approximate, transformed to local coordinates, and with full sweep.
     * * Local coordinates allows us to ignore z in determining approximation error.
     * * Full sweep guarantees we have the first quadrant in which to do our computations.
    */
    get fullEllipseXY() {
        return this._fullEllipseXY;
    }
    computeRadiansStrictlyInsideQuadrant1(result) {
        if (!this._context.isValidEllipticalArc)
            return [];
        const rangeQ1 = QuadrantFractions.getQ1FractionalRange(this.fullEllipseXY.sweep);
        const q1 = [QuadrantFractions.create(1, [rangeQ1.low, rangeQ1.high], true, true)];
        const processor = AdaptiveSubdivisionQ1ErrorProcessor.create(this.fullEllipseXY, this._options.maxError);
        do {
            EllipticalArcApproximationContext.processQuadrantFractions(this.fullEllipseXY, q1, processor);
        } while (processor.isRefined);
        return processor.getRefinedInteriorQ1Angles(result);
    }
}
;
/**
 * Processor for constructing a sample-based circular arc chain approximation.
 * @internal
 */
class ArcChainConstructionProcessor extends QuadrantFractionsProcessor {
    _chain;
    _quadrantChain;
    constructor(ellipticalArc, forcePath) {
        super();
        this._chain = (ellipticalArc.sweep.isFullCircle && !forcePath) ? Loop.create() : Path.create();
    }
    static create(ellipticalArc, forcePath = false) {
        return new ArcChainConstructionProcessor(ellipticalArc, forcePath);
    }
    get chain() {
        return this._chain.children.length > 0 ? this._chain : undefined;
    }
    announceQuadrantBegin(_q, _reversed) {
        this._quadrantChain = undefined;
        return true;
    }
    announceArc(arc, _fPrev, _f0, _f1) {
        if (!this._quadrantChain)
            this._quadrantChain = Path.create(); // the arc chain in a quadrant is always open
        this._quadrantChain.tryAddChild(arc); // captured!
    }
    announceQuadrantEnd(_q, reversed) {
        if (this._quadrantChain) {
            if (reversed)
                this._quadrantChain.reverseChildrenInPlace();
            for (const child of this._quadrantChain.children)
                this._chain.tryAddChild(child); // captured!
        }
    }
}
;
/**
 * Context for sampling a non-circular Arc3d and for constructing an approximation to it based on interpolation
 * of the samples.
 * * [[EllipticalArcApproximationContext.constructCircularArcChainApproximation]] constructs a `CurveChain`
 * approximation consisting of circular arcs.
 * * Various sample methods are supported, cf. [[EllipticalArcApproximationOptions]].
 * @internal
 */
export class EllipticalArcApproximationContext {
    _ellipticalArc;
    _localToWorld;
    _isValidEllipticalArc;
    static workPt0 = Point3d.createZero();
    static workPt1 = Point3d.createZero();
    static workPt2 = Point3d.createZero();
    static workRay = Ray3d.createZero();
    /** Constructor, captures input. */
    constructor(ellipticalArc) {
        this._isValidEllipticalArc = false;
        const data = ellipticalArc.toScaledMatrix3d();
        this._ellipticalArc = Arc3d.createScaledXYColumns(data.center, data.axes, data.r0, data.r90, data.sweep); // work on the major-minor axis version of the arc to take advantage of its symmetry
        this._localToWorld = Transform.createRefs(data.center, data.axes);
        if (this._localToWorld.matrix.isSingular())
            return;
        if (this._ellipticalArc.sweep.isEmpty)
            return; // ellipse must have a nonzero sweep
        const xMag2 = ellipticalArc.matrixRef.columnXMagnitudeSquared();
        const yMag2 = ellipticalArc.matrixRef.columnYMagnitudeSquared();
        if (Geometry.isSmallMetricDistanceSquared(xMag2) || Geometry.isSmallMetricDistanceSquared(yMag2))
            return; // ellipse must have positive radii
        if (Geometry.isSameCoordinateSquared(xMag2, yMag2))
            return; // ellipse must not be circular
        this._isValidEllipticalArc = true;
    }
    /** Constructor, clones input. */
    static create(ellipticalArc) {
        return new EllipticalArcApproximationContext(ellipticalArc);
    }
    /**
     * The arc to be sampled.
     * * Its axes are forced to be perpendicular.
     * * It is stored in world coordinates.
     */
    get ellipticalArc() {
        return this._ellipticalArc;
    }
    /**
     * The rigid transformation that maps `ellipticalArc` from local coordinates to world coordinates.
     * * In local coordinates, the arc center lies at the origin and its (perpendicular) axes of symmetry lie along
     * the positive x- and y-axes.
     */
    get localToWorld() {
        return this._localToWorld;
    }
    /**
     * Whether the elliptical arc is amenable to sampling.
     * * The arc is valid if it is non-circular, has nonzero sweep, and has positive radii (nonsingular matrix).
     */
    get isValidEllipticalArc() {
        return this._isValidEllipticalArc;
    }
    /**
     * Create a clone of the context's arc in local coordinates (with axes aligned to x-axis and y-axis).
     * @param fullSweep Optionally set full sweep on the returned local arc. Start angle is preserved.
     * @returns local arc, or undefined if the arc is invalid.
     */
    cloneLocalArc(fullSweep) {
        if (!this.isValidEllipticalArc)
            return undefined;
        const worldToLocal = this.localToWorld.inverse();
        if (!worldToLocal)
            return undefined;
        const arcXY = this.ellipticalArc.cloneTransformed(worldToLocal);
        if (fullSweep) {
            let sweep = 2 * Math.PI;
            if (!arcXY.sweep.isCCW)
                sweep = -sweep;
            arcXY.sweep.setStartEndRadians(arcXY.sweep.startRadians, arcXY.sweep.startRadians + sweep);
        }
        return arcXY;
    }
    /**
     * Process structured sample data for the given elliptical arc.
     * * Circular arcs are announced to the processor for each sample interval in each quadrant.
     * * Each quadrant is processed separately to allow the elliptical arc's axis points and tangents to be interpolated.
     * * A 2-point plus tangent construction is used to create the first and last circular arc in each quadrant.
     * * Symmetry of the announced circular arcs matching that of a multi-quadrant spanning elliptical arc is ensured by
     * processing the samples consistently, starting along the elliptical arc's major axis in each quadrant.
     * @param ellipticalArc source arc to approximate.
     * @param quadrants structured samples, may be temporarily reversed for symmetry.
     * @param processor callbacks for handling the constructed arcs.
     * @internal
    */
    static processQuadrantFractions(ellipticalArc, quadrants, processor) {
        const pt0 = this.workPt0;
        const pt1 = this.workPt1;
        const pt2 = this.workPt2;
        const ray = this.workRay;
        const arcBetween2Samples = (arcStart, arcEnd, reverse) => {
            // assume non-colinear inputs
            const myArc = Arc3d.createCircularStartTangentEnd(arcStart.origin, arcStart.direction, arcEnd);
            if (!(myArc instanceof Arc3d))
                return undefined;
            if (reverse)
                myArc.reverseInPlace();
            return myArc;
        };
        const arcBetweenLast2Of3Samples = (p0, arcStart, arcEnd) => {
            // assume non-colinear inputs; initial arc starts at p0, ends at arcEnd
            const arc = Arc3d.createCircularStartMiddleEnd(p0, arcStart, arcEnd);
            if (!(arc instanceof Arc3d))
                return undefined; // colinear?
            const startAngle = arc.vector0.signedAngleTo(Vector3d.createStartEnd(arc.center, arcStart), arc.matrixRef.columnZ());
            arc.sweep.setStartEndRadians(startAngle.radians, arc.sweep.endRadians);
            return arc; // returned arc starts at arcStart, ends at arcEnd
        };
        const createFirstArc = (f0, f1) => {
            // This arc starts at the first sample f0 and ends at f1.
            // This arc interpolates point and tangent at f0, but only point at f1.
            ellipticalArc.fractionToPointAndDerivative(f0, ray);
            ellipticalArc.fractionToPoint(f1, pt1);
            if (f0 > f1)
                ray.direction.scaleInPlace(-1); // computed arc is retrograde
            const arc = arcBetween2Samples(ray, pt1, false);
            if (arc)
                processor.announceArc(arc, undefined, f0, f1);
        };
        const createInnerArc = (f0, f1, f2) => {
            // This arc starts at f1 and ends at f2
            let fPrev = f0;
            if (processor.getPreviousFraction)
                fPrev = processor.getPreviousFraction(f1) ?? f0;
            ellipticalArc.fractionToPoint(fPrev, pt0);
            ellipticalArc.fractionToPoint(f1, pt1);
            ellipticalArc.fractionToPoint(f2, pt2);
            const arc = arcBetweenLast2Of3Samples(pt0, pt1, pt2);
            if (arc)
                processor.announceArc(arc, fPrev, f1, f2);
        };
        const createLastArc = (f0, f1) => {
            // This arc starts at f0 and ends at the last sample f1.
            // This arc interpolates point and tangent at f1, but only point at f0.
            ellipticalArc.fractionToPoint(f0, pt0);
            ellipticalArc.fractionToPointAndDerivative(f1, ray);
            if (f1 > f0)
                ray.direction.scaleInPlace(-1); // computed arc is retrograde
            // compute last arc from f1 to f0, then reverse
            const arc = arcBetween2Samples(ray, pt0, true);
            if (arc)
                processor.announceArc(arc, undefined, f0, f1);
        };
        const reverseFractionsForSymmetry = (q) => {
            // If q interpolates an axis, we process q.fractions in a consistent direction (increasing or decreasing) so that the
            // approximating arc chain exhibits fourfold axial symmetry. We do this by ensuring q.fractions starts along the
            // major axis (or ends along the minor axis). This choice is arbitrary, but consistently made across all quadrants.
            if (!q.interpolateStartTangent && !q.interpolateEndTangent)
                return false;
            const n = q.fractions.length;
            if (n < 2)
                return false;
            const xAxisIsMajor = ellipticalArc.vector0.magnitudeSquared() > ellipticalArc.vector90.magnitudeSquared();
            const processCCWQuadrantInReverse = xAxisIsMajor ? (q.quadrant === 2 || q.quadrant === 4) : (q.quadrant === 1 || q.quadrant === 3);
            const isAlreadyReversed = q.fractions[0] > q.fractions[n - 1];
            const doReverse = !isAlreadyReversed && (processCCWQuadrantInReverse === ellipticalArc.sweep.isCCW);
            if (doReverse)
                q.reverse(); // for symmetry we sometimes process decreasing fractions. This creates slightly different arcs.
            return doReverse;
        };
        for (const q of quadrants) {
            const n = q.fractions.length;
            if (n < 2)
                continue;
            const reversed = reverseFractionsForSymmetry(q);
            if (!processor.announceQuadrantBegin(q, reversed))
                continue;
            if (q.interpolateStartTangent)
                createFirstArc(q.fractions[0], q.fractions[1]);
            // the first inner arc approximates the ellipse over [f[1],f[2]]; the last inner arc, over [f[n-3],f[n-2]]
            for (let i = 0; i + 2 < n - 1; ++i)
                createInnerArc(q.fractions[i], q.fractions[i + 1], q.fractions[i + 2]);
            if (n > 2) { // the final arc approximates [f[n-2],f[n-1]]
                if (q.interpolateEndTangent)
                    createLastArc(q.fractions[n - 2], q.fractions[n - 1]);
                else
                    createInnerArc(q.fractions[n - 3], q.fractions[n - 2], q.fractions[n - 1]);
            }
            processor.announceQuadrantEnd(q, reversed);
            if (reversed)
                q.reverse(); // undo the reverse above
        }
    }
    /**
     * Compute the maximum error of the circular arc chain approximation determined by the given samples.
     * * This is measured by the longest perpendicular between the elliptical arc and its approximation.
     * @param samples structured sample data from the instance's elliptical arc.
     * @return details of the perpendicular measuring the max approximation error, or undefined if no such perpendicular.
     * For each of `detailA` and `detailB`:
     * * `point` is the end of the perpendicular on each curve.
     * * `fraction` is the curve parameter of the point.
     * * `a` is the distance between the points.
     * @internal
     */
    computeApproximationError(samples) {
        const arcXY = this.cloneLocalArc();
        if (!arcXY)
            return undefined;
        const processor = ArcChainErrorProcessor.create(arcXY);
        EllipticalArcApproximationContext.processQuadrantFractions(arcXY, samples, processor);
        const maxError = processor.maxPerpendicular;
        return (maxError && maxError.tryTransformInPlace(this.localToWorld)) ? maxError : undefined;
    }
    /**
     * Compute samples for the elliptical arc as fraction parameters.
     * * This method houses the sampling framework for all sampling methods, which are customized via implementations
     * of the [[EllipticalArcSampler]] interface.
     * * Note that the returned samples are fractions in the parameterization of the context's arc (whose axes have been
     * forced to be perpendicular), not the input arc passed into the context's constructor.
     * @param options options that determine how the elliptical arc is sampled.
     * @param structuredOutput flag indicating output format as follows:
     * * If false (default), return all fractions in one sorted (increasing), deduplicated array (a full ellipse includes
     * both 0 and 1).
     * * If true, fractions are assembled by quadrants:
     *   * Each [[QuadrantFractions]] object holds at least three sorted (increasing), deduplicated fractions in a
     * specified quadrant of the arc.
     *   * If only two fractions would be computed for a given `QuadrantFractions`, their midpoint is inserted to enable
     * tangent interpolation at both ends. Such a quadrant `q` is marked with `q.averageAdded = true`.
     *   * The `QuadrantFractions` objects themselves are sorted by increasing order of the fractions they contain.
     *   * If the arc sweep spans adjacent quadrants, the fraction bordering the quadrants appears in both `QuadrantFractions`.
     *   * If the arc starts and ends in the same quadrant, two `QuadrantFractions` objects can be returned.
     *   * This means there are between 1 and 5 objects in the `QuadrantFractions` array.
     * @internal
     */
    computeSampleFractions(options, structuredOutput = false) {
        if (!this.isValidEllipticalArc)
            return [];
        const compareRadiansIncreasing = (a0, a1) => {
            if (Geometry.isAlmostEqualNumber(a0, a1, Geometry.smallAngleRadians))
                return 0;
            return a0 < a1 ? -1 : 1;
        };
        const compareRadiansDecreasing = (a0, a1) => {
            if (Geometry.isAlmostEqualNumber(a0, a1, Geometry.smallAngleRadians))
                return 0;
            return a0 < a1 ? 1 : -1;
        };
        const compareQuadrantFractions = (q0, q1) => {
            // ASSUME QuadrantFractions.fractions arrays are sorted (increasing) and have only trivial overlap
            if (compareFractionsIncreasing(q0.fractions[q0.fractions.length - 1], q1.fractions[0]) <= 0)
                return -1;
            if (compareFractionsIncreasing(q1.fractions[q1.fractions.length - 1], q0.fractions[0]) <= 0)
                return 1;
            return 0;
        };
        const shiftRadiansToSweep = (angle, sweep) => {
            const inSweep = sweep.isRadiansInSweep(angle, true);
            if (inSweep) {
                const fraction = sweep.radiansToSignedPeriodicFraction(angle);
                if (Geometry.isIn01(fraction))
                    angle = sweep.fractionToRadians(fraction);
            }
            return { angle, inSweep };
        };
        const convertAndAddRadiansToFractionInRange = (dest, radians, sweep, f0, f1) => {
            if (undefined === f0)
                f0 = 0;
            if (undefined === f1)
                f1 = 1;
            if (f0 > f1)
                return convertAndAddRadiansToFractionInRange(dest, radians, sweep, f1, f0);
            const fraction = sweep.radiansToSignedPeriodicFraction(radians);
            if (fraction < (f0 - Geometry.smallFraction) || (f1 + Geometry.smallFraction) < fraction)
                return undefined; // angle is outside sweep
            Geometry.restrictToInterval(fraction, 0, 1);
            dest.add(fraction);
            return fraction;
        };
        const convertQ1RadiansInSweepToQuadrantFractions = (anglesInQ1, angle0, angle1, sweep) => {
            if (angle0 > angle1)
                return convertQ1RadiansInSweepToQuadrantFractions(anglesInQ1, angle1, angle0, sweep);
            if (Angle.isAlmostEqualRadiansNoPeriodShift(angle0, angle1))
                return undefined; // empty sweep
            const qData = QuadrantFractions.getQuadrantRadians(angle0, angle1);
            if (undefined === qData)
                return undefined; // no containing quadrant
            const qFractions = new OrderedSet(compareFractionsIncreasing);
            const f0 = convertAndAddRadiansToFractionInRange(qFractions, angle0, sweep);
            const f1 = convertAndAddRadiansToFractionInRange(qFractions, angle1, sweep);
            if (undefined === f0 || undefined === f1)
                return undefined;
            for (const a0 of anglesInQ1) {
                let angle = a0;
                if (2 === qData.quadrant)
                    angle = Angle.piRadians - angle;
                else if (3 === qData.quadrant)
                    angle = Angle.piRadians + angle;
                else if (4 === qData.quadrant)
                    angle = Angle.pi2Radians - angle;
                convertAndAddRadiansToFractionInRange(qFractions, angle, sweep, f0, f1);
            }
            const qf = QuadrantFractions.create(qData.quadrant, [...qFractions]);
            const n = qf.fractions.length;
            if (2 === n) { // e.g. elliptical arc is so small it contains no interior samples in this quadrant
                qf.fractions.splice(1, 0, Geometry.interpolate(qf.fractions[0], 0.5, qf.fractions[1]));
                qf.averageAdded = true;
            }
            return qf;
        };
        const computeStructuredOutput = (anglesInQ1, arcSweep) => {
            const qEndAngles = new OrderedSet(arcSweep.isCCW ? compareRadiansIncreasing : compareRadiansDecreasing);
            qEndAngles.add(arcSweep.endRadians);
            for (const qAngle of [0, Angle.piOver2Radians, Angle.piRadians, Angle.pi3Over2Radians, Angle.pi2Radians]) {
                const shifted = shiftRadiansToSweep(qAngle, arcSweep);
                if (shifted.inSweep)
                    qEndAngles.add(shifted.angle);
            }
            const quadrants = new OrderedSet(compareQuadrantFractions);
            let a0 = arcSweep.startRadians;
            for (const a1 of qEndAngles) {
                const quadrant = convertQ1RadiansInSweepToQuadrantFractions(anglesInQ1, a0, a1, arcSweep);
                if (quadrant)
                    quadrants.add(quadrant);
                a0 = a1;
            }
            return [...quadrants];
        };
        const computeFlatOutput = (anglesInQ1, arcSweep) => {
            // first add the quadrant fractions so the set prefers them over nearby interior fractions
            const fractions = new OrderedSet(compareFractionsIncreasing);
            fractions.add(0);
            fractions.add(1);
            for (const angle of [0, Angle.piOver2Radians, Angle.piRadians, Angle.pi3Over2Radians])
                convertAndAddRadiansToFractionInRange(fractions, angle, arcSweep);
            // add interior Q1 fractions, reflect to the other quadrants, filter by sweep and extant entry
            for (const angle0 of anglesInQ1) {
                for (const angle of [angle0, Angle.piRadians - angle0, Angle.piRadians + angle0, Angle.pi2Radians - angle0])
                    convertAndAddRadiansToFractionInRange(fractions, angle, arcSweep);
            }
            return [...fractions];
        };
        // sample the (full) ellipse as angles in strict interior of Quadrant 1
        const radiansQ1 = []; // unordered
        switch (options.sampleMethod) {
            case EllipticalArcSampleMethod.UniformParameter: {
                UniformParameterSampler.create(this, options).computeRadiansStrictlyInsideQuadrant1(radiansQ1);
                break;
            }
            case EllipticalArcSampleMethod.UniformCurvature: {
                UniformCurvatureSampler.create(this, options).computeRadiansStrictlyInsideQuadrant1(radiansQ1);
                break;
            }
            case EllipticalArcSampleMethod.NonUniformCurvature: {
                NonUniformCurvatureSampler.create(this, options).computeRadiansStrictlyInsideQuadrant1(radiansQ1);
                break;
            }
            case EllipticalArcSampleMethod.AdaptiveSubdivision: {
                AdaptiveSubdivisionSampler.create(this, options).computeRadiansStrictlyInsideQuadrant1(radiansQ1);
                break;
            }
            default:
                break;
        }
        return structuredOutput ?
            computeStructuredOutput(radiansQ1, this.ellipticalArc.sweep) :
            computeFlatOutput(radiansQ1, this.ellipticalArc.sweep);
    }
    /** Construct a circular arc chain approximation to the elliptical arc. */
    constructCircularArcChainApproximation(options) {
        if (!this.isValidEllipticalArc)
            return undefined;
        if (!options)
            options = EllipticalArcApproximationOptions.create();
        const samples = this.computeSampleFractions(options, true);
        const processor = ArcChainConstructionProcessor.create(this.ellipticalArc, options.forcePath);
        EllipticalArcApproximationContext.processQuadrantFractions(this.ellipticalArc, samples, processor);
        return processor.chain;
    }
}
//# sourceMappingURL=EllipticalArcApproximationContext.js.map